Skip to content

Commit 205a4e4

Browse files
authored
hyperv_ic: correlate responses with outstanding requests (#5029)
IC requests currently reuse transaction ID zero and accept replies with unexpected protocol versions. A delayed response can therefore complete a later request. Assign advancing transaction IDs and ignore replies that do not match the outstanding request or expected protocol versions. Preserve transaction identity across shutdown save/restore, including older snapshots, and require shutdown acknowledgments to have the shutdown message type. Regression tests cover stale replies, invalid IDs and versions, wraparound, and save/restore. All 16 package tests pass on Windows and Linux.
1 parent aa4765f commit 205a4e4

4 files changed

Lines changed: 541 additions & 18 deletions

File tree

‎vm/devices/hyperv_ic/src/common.rs‎

Lines changed: 212 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub(crate) struct IcPipe<M: RingMem = GpadlRingMem> {
3232
pub pipe: MessagePipe<M>,
3333
#[inspect(skip)]
3434
buf: Vec<u8>,
35+
pub transaction_id: u8,
3536
}
3637

3738
#[derive(Inspect, Default)]
@@ -54,7 +55,11 @@ impl<M: RingMem> IcPipe<M> {
5455
pub fn new(raw: RawAsyncChannel<M>) -> Result<Self, std::io::Error> {
5556
let pipe = MessagePipe::new(raw)?;
5657
let buf = vec![0; hyperv_ic_protocol::MAX_MESSAGE_SIZE];
57-
Ok(Self { pipe, buf })
58+
Ok(Self {
59+
pipe,
60+
buf,
61+
transaction_id: u8::MAX,
62+
})
5863
}
5964

6065
pub async fn negotiate(
@@ -64,6 +69,7 @@ impl<M: RingMem> IcPipe<M> {
6469
) -> anyhow::Result<Option<Versions>> {
6570
match state {
6671
NegotiateState::SendVersion => {
72+
let transaction_id = self.transaction_id.wrapping_add(1);
6773
let message = hyperv_ic_protocol::NegotiateMessage {
6874
framework_version_count: FRAMEWORK_VERSIONS.len() as u16,
6975
message_version_count: message_versions.len() as u16,
@@ -76,7 +82,7 @@ impl<M: RingMem> IcPipe<M> {
7682
+ size_of_val(FRAMEWORK_VERSIONS)
7783
+ size_of_val(message_versions)) as u16,
7884
status: Status::SUCCESS,
79-
transaction_id: 0,
85+
transaction_id,
8086
flags: HeaderFlags::new().with_transaction(true).with_request(true),
8187
..FromZeros::new_zeroed()
8288
};
@@ -91,11 +97,14 @@ impl<M: RingMem> IcPipe<M> {
9197
.await
9298
.context("ring buffer error")?;
9399

100+
self.transaction_id = transaction_id;
94101
*state = NegotiateState::WaitVersion;
95102
Ok(None)
96103
}
97104
NegotiateState::WaitVersion => {
98-
let (message_type, status, buf) = self.read_message().await?;
105+
let Some((message_type, status, buf)) = self.read_message(None).await? else {
106+
return Ok(None);
107+
};
99108
// A response to a request issued before the channel was
100109
// reset (e.g. across save/restore) may still be in flight.
101110
// There is no one left to receive it, so drop it.
@@ -148,29 +157,40 @@ impl<M: RingMem> IcPipe<M> {
148157
flags: HeaderFlags,
149158
message: &[u8],
150159
) -> anyhow::Result<()> {
160+
let transaction_id = self.transaction_id.wrapping_add(1);
151161
let header = hyperv_ic_protocol::Header {
152162
framework_version: versions.framework_version,
153163
message_type,
154164
message_size: message.len() as u16,
155165
message_version: versions.message_version,
156166
status: Status::SUCCESS,
157-
transaction_id: 0,
167+
transaction_id,
158168
flags,
159169
..FromZeros::new_zeroed()
160170
};
161171

162172
self.pipe
163173
.send_vectored(&[IoSlice::new(header.as_bytes()), IoSlice::new(message)])
164174
.await
165-
.context("ring buffer error")
175+
.context("ring buffer error")?;
176+
self.transaction_id = transaction_id;
177+
Ok(())
166178
}
167179

168-
pub async fn read_response(&mut self) -> anyhow::Result<(Status, &[u8])> {
169-
let (_message_type, status, buf) = self.read_message().await?;
170-
Ok((status, buf))
180+
pub async fn read_response(
181+
&mut self,
182+
versions: &Versions,
183+
) -> anyhow::Result<Option<(Status, &[u8])>> {
184+
Ok(self
185+
.read_message(Some(versions))
186+
.await?
187+
.map(|(_, status, buf)| (status, buf)))
171188
}
172189

173-
async fn read_message(&mut self) -> anyhow::Result<(MessageType, Status, &[u8])> {
190+
pub async fn read_message(
191+
&mut self,
192+
versions: Option<&Versions>,
193+
) -> anyhow::Result<Option<(MessageType, Status, &[u8])>> {
174194
let n = self
175195
.pipe
176196
.recv(&mut self.buf)
@@ -181,15 +201,42 @@ impl<M: RingMem> IcPipe<M> {
181201
.ok()
182202
.context("missing header")?;
183203

184-
if header.transaction_id != 0 || !header.flags.transaction() || !header.flags.response() {
204+
if !header.flags.transaction() || !header.flags.response() {
185205
anyhow::bail!("invalid transaction response");
186206
}
187207

208+
if header.transaction_id != self.transaction_id {
209+
tracelimit::warn_ratelimited!(
210+
transaction_id = header.transaction_id,
211+
expected_transaction_id = self.transaction_id,
212+
"dropping response for an unexpected transaction"
213+
);
214+
return Ok(None);
215+
}
216+
188217
let rest = rest
189218
.get(..header.message_size as usize)
190219
.context("missing message body")?;
191220

192-
Ok((header.message_type, header.status, rest))
221+
let valid_versions = if let Some(versions) = versions {
222+
header.framework_version == versions.framework_version
223+
&& header.message_version == versions.message_version
224+
} else if header.message_type == MessageType::VERSION_NEGOTIATION {
225+
header.framework_version == Version::new(0, 0)
226+
&& header.message_version == Version::new(0, 0)
227+
} else {
228+
true
229+
};
230+
if !valid_versions {
231+
tracelimit::warn_ratelimited!(
232+
framework_version = %header.framework_version,
233+
message_version = %header.message_version,
234+
"dropping response with incorrect protocol versions"
235+
);
236+
return Ok(None);
237+
}
238+
239+
Ok(Some((header.message_type, header.status, rest)))
193240
}
194241
}
195242

@@ -265,6 +312,160 @@ mod tests {
265312
assert!(header.flags.request());
266313
}
267314

315+
#[async_test]
316+
async fn transactions_advance_and_wrap_ids() {
317+
let (mut pipe, mut guest) = new_pipes();
318+
let mut state = NegotiateState::default();
319+
send_version_request(&mut pipe, &mut state, &mut guest).await;
320+
let negotiation_response = version_response(FRAMEWORK_VERSION_3, TEST_MESSAGE_VERSION);
321+
guest.send(&negotiation_response).await.unwrap();
322+
let versions = pipe
323+
.negotiate(&mut state, TEST_MESSAGE_VERSIONS)
324+
.await
325+
.unwrap()
326+
.unwrap();
327+
for expected_id in 1..=257u16 {
328+
pipe.write_message(
329+
&versions,
330+
MessageType::SHUTDOWN,
331+
HeaderFlags::new().with_transaction(true).with_request(true),
332+
&[],
333+
)
334+
.await
335+
.unwrap();
336+
let mut buffer = [0; hyperv_ic_protocol::MAX_MESSAGE_SIZE];
337+
let size = guest.recv(&mut buffer).await.unwrap();
338+
let (mut header, _) = Header::read_from_prefix(&buffer[..size]).unwrap();
339+
assert_eq!(header.transaction_id, expected_id as u8);
340+
if header.transaction_id == 0 {
341+
guest.send(&negotiation_response).await.unwrap();
342+
assert!(pipe.read_response(&versions).await.unwrap().is_none());
343+
assert_eq!(pipe.transaction_id, header.transaction_id);
344+
}
345+
header.flags = HeaderFlags::new()
346+
.with_transaction(true)
347+
.with_response(true);
348+
guest.send(header.as_bytes()).await.unwrap();
349+
let (status, body) = pipe.read_response(&versions).await.unwrap().unwrap();
350+
assert_eq!(status, Status::SUCCESS);
351+
assert!(body.is_empty());
352+
}
353+
}
354+
355+
#[async_test]
356+
async fn responses_match_transaction_and_versions() {
357+
let versions = Versions {
358+
framework_version: FRAMEWORK_VERSION_3,
359+
message_version: TEST_MESSAGE_VERSION,
360+
};
361+
for message_type in [
362+
MessageType::SHUTDOWN,
363+
MessageType::TIME_SYNC,
364+
MessageType::KVP_EXCHANGE,
365+
] {
366+
let (mut pipe, mut guest) = new_pipes();
367+
let mut buffer = [0; hyperv_ic_protocol::MAX_MESSAGE_SIZE];
368+
for expected_id in 0..2 {
369+
pipe.write_message(
370+
&versions,
371+
message_type,
372+
HeaderFlags::new().with_transaction(true).with_request(true),
373+
&[],
374+
)
375+
.await
376+
.unwrap();
377+
let size = guest.recv(&mut buffer).await.unwrap();
378+
let (mut response, _) = Header::read_from_prefix(&buffer[..size]).unwrap();
379+
assert_eq!(response.transaction_id, expected_id);
380+
response.flags = HeaderFlags::new()
381+
.with_transaction(true)
382+
.with_response(true);
383+
384+
for status in [Status::SUCCESS, Status::FAIL] {
385+
let mut negotiation_response =
386+
version_response(FRAMEWORK_VERSION_3, TEST_MESSAGE_VERSION);
387+
let (header, _) =
388+
Header::mut_from_prefix(negotiation_response.as_mut_slice()).unwrap();
389+
header.transaction_id = expected_id;
390+
header.status = status;
391+
guest.send(&negotiation_response).await.unwrap();
392+
assert!(pipe.read_response(&versions).await.unwrap().is_none());
393+
assert_eq!(pipe.transaction_id, expected_id);
394+
}
395+
396+
if expected_id == 1 {
397+
for (transaction_id, framework_version, message_version) in [
398+
(0, versions.framework_version, versions.message_version),
399+
(2, versions.framework_version, versions.message_version),
400+
(1, Version::new(0, 0), versions.message_version),
401+
(1, FRAMEWORK_VERSION_1, versions.message_version),
402+
(1, Version::new(3, 1), versions.message_version),
403+
(1, versions.framework_version, Version::new(0, 0)),
404+
(1, versions.framework_version, Version::new(5, 1)),
405+
] {
406+
let invalid = Header {
407+
transaction_id,
408+
framework_version,
409+
message_version,
410+
status: Status::NOT_SUPPORTED,
411+
..response
412+
};
413+
guest.send(invalid.as_bytes()).await.unwrap();
414+
assert!(pipe.read_response(&versions).await.unwrap().is_none());
415+
assert_eq!(pipe.transaction_id, expected_id);
416+
}
417+
response.status = Status::FAIL;
418+
}
419+
420+
let body = b"ack";
421+
response.message_size = body.len() as u16;
422+
guest
423+
.send(&[response.as_bytes(), body].concat())
424+
.await
425+
.unwrap();
426+
let (status, received_body) = pipe.read_response(&versions).await.unwrap().unwrap();
427+
assert_eq!(status, response.status);
428+
assert_eq!(received_body, body);
429+
}
430+
}
431+
}
432+
433+
#[async_test]
434+
async fn negotiate_drops_unmatched_ids_and_invalid_header_versions() {
435+
let (mut pipe, mut guest) = new_pipes();
436+
let mut state = NegotiateState::default();
437+
send_version_request(&mut pipe, &mut state, &mut guest).await;
438+
for (transaction_id, framework_version, message_version) in [
439+
(1, Version::new(0, 0), Version::new(0, 0)),
440+
(0, FRAMEWORK_VERSION_3, Version::new(0, 0)),
441+
(0, Version::new(0, 0), TEST_MESSAGE_VERSION),
442+
] {
443+
let mut response = version_response(FRAMEWORK_VERSION_3, TEST_MESSAGE_VERSION);
444+
let (header, _) = Header::mut_from_prefix(response.as_mut_slice()).unwrap();
445+
header.transaction_id = transaction_id;
446+
header.framework_version = framework_version;
447+
header.message_version = message_version;
448+
guest.send(&response).await.unwrap();
449+
assert!(
450+
pipe.negotiate(&mut state, TEST_MESSAGE_VERSIONS)
451+
.await
452+
.unwrap()
453+
.is_none()
454+
);
455+
assert!(matches!(state, NegotiateState::WaitVersion));
456+
}
457+
guest
458+
.send(&version_response(FRAMEWORK_VERSION_3, TEST_MESSAGE_VERSION))
459+
.await
460+
.unwrap();
461+
assert!(
462+
pipe.negotiate(&mut state, TEST_MESSAGE_VERSIONS)
463+
.await
464+
.unwrap()
465+
.is_some()
466+
);
467+
}
468+
268469
#[async_test]
269470
async fn negotiate() {
270471
for &framework_version in FRAMEWORK_VERSIONS {

‎vm/devices/hyperv_ic/src/kvp.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,9 @@ impl<M: RingMem + Unpin> KvpChannel<M> {
385385
*state = ReadyState::WaitingResponse(rpc);
386386
}
387387
ReadyState::WaitingResponse(_) => {
388-
let (status, response) = self.pipe.read_response().await?;
388+
let Some((status, response)) = self.pipe.read_response(versions).await? else {
389+
return Ok(());
390+
};
389391
let r = if status == Status::SUCCESS {
390392
Ok(())
391393
} else {

0 commit comments

Comments
 (0)