Skip to content

Commit 33c8fba

Browse files
authored
Merge pull request #14 from cortexapps/engram/fix-vsock-rx-gate-plain-resume
fix(vsock): kick() must not arm the RX gate on a plain vm resume
2 parents fbe29dd + 74b66e2 commit 33c8fba

2 files changed

Lines changed: 113 additions & 14 deletions

File tree

‎src/vmm/src/devices/virtio/vsock/device.rs‎

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,23 @@ where
403403
}
404404

405405
fn kick(&mut self) {
406-
if self.is_activated() {
407-
self.pending_event_ack = true;
408-
406+
// `kick()` runs on EVERY vm resume (`Vmm::resume_vm` -> `kick_virtio_devices`),
407+
// which covers three distinct paths:
408+
// - snapshot-restore resume: the source's `prepare_save` queued a
409+
// `TRANSPORT_RESET_EVENT` into the (snapshotted) used ring, and `restore()`
410+
// re-armed `pending_event_ack` from the saved activation flag;
411+
// - post-snapshot source resume (diff checkpoints): `prepare_save` queued the
412+
// reset and armed the gate in this very process;
413+
// - PLAIN pause->resume (no snapshot anywhere): nothing was queued and nothing
414+
// must be signaled.
415+
// Arming the gate *here* used to break the third path: with no reset queued
416+
// there is nothing for the guest to ack, so the gate never cleared — all
417+
// host->guest RX black-holed and the event loop busy-spun on the undeliverable
418+
// backend backlog (the ADR 0074 rung-2 parked-paused un-pause wedge). So:
419+
// signal only when a reset is genuinely awaiting the guest's ack, and never arm
420+
// from here — `prepare_save` (in-process) and `restore()` (cross-snapshot) own
421+
// the arming.
422+
if self.is_activated() && self.pending_event_ack {
409423
// Vsock has a complicated protocol that isn't resilient to any packet loss,
410424
// so for Vsock we don't support connection persistence through snapshot. Any
411425
// in-flight packets or events are simply lost and Vsock is restored 'empty'.
@@ -618,39 +632,71 @@ mod tests {
618632
}
619633

620634
#[test]
621-
fn test_kick_when_active_arms_pending_event_ack() {
622-
// Restore path: kick() is invoked after the snapshot is loaded to re-deliver the
623-
// TRANSPORT_RESET interrupt. It must arm the RX gate so the post-restore RX/EVQ
624-
// race cannot deliver data ahead of the guest ack.
635+
fn test_kick_when_armed_keeps_gate_and_suppresses_rx() {
636+
// Restore / post-snapshot-source resume: a TRANSPORT_RESET is awaiting the
637+
// guest's ack (`pending_event_ack` was armed by prepare_save or restore()).
638+
// kick() must re-signal while KEEPING the gate armed, so the post-restore
639+
// RX/EVQ race cannot deliver data ahead of the guest ack.
625640
let test_ctx = TestContext::new();
626641
let mut ctx = test_ctx.create_event_handler_context();
627642
ctx.mock_activate(test_ctx.mem.clone(), test_ctx.interrupt.clone());
628643

629-
ctx.device.pending_event_ack = false;
644+
ctx.device.pending_event_ack = true;
630645
ctx.device.kick();
631646

632647
assert!(
633648
ctx.device.pending_event_ack,
634-
"kick() on an active device must arm the RX gate"
649+
"kick() must keep the RX gate armed while the reset awaits its ack"
635650
);
636651

637-
// After kick(), the gate must actually suppress RX delivery.
652+
// With the gate armed, RX delivery must stay suppressed.
638653
ctx.device.backend.set_pending_rx(true);
639654
let progressed = ctx.device.process_rx().unwrap();
640655
assert!(!progressed);
641656
assert_eq!(ctx.guest_rxvq.used.idx.get(), 0);
642657
}
643658

659+
#[test]
660+
fn test_kick_plain_resume_unarmed_is_a_noop() {
661+
// PLAIN pause->resume (no snapshot anywhere): no TRANSPORT_RESET was queued, so
662+
// kick() must neither arm the gate nor signal any queue. Arming here is the
663+
// ADR 0074 rung-2 wedge: nothing for the guest to ack -> the gate never clears
664+
// -> all host->guest RX black-holes and the event loop busy-spins.
665+
let test_ctx = TestContext::new();
666+
let mut ctx = test_ctx.create_event_handler_context();
667+
ctx.mock_activate(test_ctx.mem.clone(), test_ctx.interrupt.clone());
668+
669+
assert!(!ctx.device.pending_event_ack);
670+
ctx.device.kick();
671+
672+
assert!(
673+
!ctx.device.pending_event_ack,
674+
"kick() must never arm the RX gate itself"
675+
);
676+
// No queue eventfd may be signaled on an unarmed kick (non-blocking read
677+
// errors when the eventfd has no pending count).
678+
ctx.device.queue_events[TXQ_INDEX].read().unwrap_err();
679+
ctx.device.queue_events[RXQ_INDEX].read().unwrap_err();
680+
ctx.device.queue_events[EVQ_INDEX].read().unwrap_err();
681+
682+
// And RX must still flow: the un-paused guest's connections are intact.
683+
ctx.device.backend.set_pending_rx(true);
684+
let progressed = ctx.device.process_rx().unwrap();
685+
assert!(progressed, "RX must not be gated after a plain resume");
686+
}
687+
644688
#[test]
645689
fn test_kick_replays_tx_notification_only() {
646-
// On restore, kick() must replay only the TX data queue (to re-process in-flight
647-
// TX and re-arm avail_event). RX is gated by pending_event_ack so it needs no
648-
// replay, and the event queue's data eventfd must not be notified -- that is the
649-
// guest's TRANSPORT_RESET ack path; the event queue is signaled host->guest.
690+
// On restore (gate armed), kick() must replay only the TX data queue (to
691+
// re-process in-flight TX and re-arm avail_event). RX is gated by
692+
// pending_event_ack so it needs no replay, and the event queue's data eventfd
693+
// must not be notified -- that is the guest's TRANSPORT_RESET ack path; the
694+
// event queue is signaled host->guest.
650695
let test_ctx = TestContext::new();
651696
let mut ctx = test_ctx.create_event_handler_context();
652697
ctx.mock_activate(test_ctx.mem.clone(), test_ctx.interrupt.clone());
653698

699+
ctx.device.pending_event_ack = true;
654700
ctx.device.kick();
655701

656702
// TX queue eventfd was replayed for re-processing.

‎src/vmm/src/devices/virtio/vsock/persist.rs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ where
114114
vsock.acked_features = state.virtio_state.acked_features;
115115
vsock.avail_features = state.virtio_state.avail_features;
116116
vsock.device_state = DeviceState::Inactive;
117+
// Every snapshot of an *activated* vsock device went through `prepare_save`,
118+
// which queued a `TRANSPORT_RESET_EVENT` into the (snapshotted) used ring and
119+
// armed the RX gate. The gate flag itself is not persisted, so re-arm it from
120+
// the saved activation flag: the restore-side `kick()` then re-signals the evq
121+
// and RX stays gated until the restored guest acks the reset. (The device is
122+
// still `Inactive` here — activation is re-applied later by the device manager
123+
// — so this must read the saved flag, not `is_activated()`.) A never-activated
124+
// snapshot queued no reset: leave the gate disarmed or RX would gate forever.
125+
vsock.pending_event_ack = state.virtio_state.activated;
117126
Ok(vsock)
118127
}
119128
}
@@ -145,6 +154,50 @@ pub(crate) mod tests {
145154
}
146155
}
147156

157+
#[test]
158+
fn test_restore_arms_rx_gate_iff_snapshot_was_activated() {
159+
// An activated snapshot always carries a queued TRANSPORT_RESET (prepare_save
160+
// runs on every save), so restore() must re-arm the RX gate for the guest to
161+
// ack. A never-activated snapshot queued no reset — arming would gate RX
162+
// forever.
163+
let ctx = TestContext::new();
164+
165+
let mut state = VsockState {
166+
backend: ctx.device.backend().save(),
167+
frontend: ctx.device.save(),
168+
};
169+
170+
// Unactivated snapshot -> gate stays disarmed.
171+
assert!(!state.frontend.virtio_state.activated);
172+
let restored = Vsock::restore(
173+
VsockConstructorArgs {
174+
mem: ctx.mem.clone(),
175+
backend: TestBackend::new(),
176+
},
177+
&state.frontend,
178+
)
179+
.unwrap();
180+
assert!(
181+
!restored.pending_event_ack,
182+
"restore of a never-activated snapshot must not arm the RX gate"
183+
);
184+
185+
// Activated snapshot -> gate re-armed.
186+
state.frontend.virtio_state.activated = true;
187+
let restored = Vsock::restore(
188+
VsockConstructorArgs {
189+
mem: ctx.mem.clone(),
190+
backend: TestBackend::new(),
191+
},
192+
&state.frontend,
193+
)
194+
.unwrap();
195+
assert!(
196+
restored.pending_event_ack,
197+
"restore of an activated snapshot must re-arm the RX gate"
198+
);
199+
}
200+
148201
#[test]
149202
fn test_persist_uds_backend() {
150203
let ctx = TestContext::new();

0 commit comments

Comments
 (0)