use super::*;
use crate::vmm::bulk::HostAssembler;
use crate::vmm::wire::{
FRAME_HEADER_SIZE, MSG_TYPE_SCHED_EXIT, MSG_TYPE_SNAPSHOT_REQUEST, MSG_TYPE_SYS_RDY,
SNAPSHOT_KIND_CAPTURE, SNAPSHOT_TAG_MAX, ShmMessage, SnapshotRequestPayload,
};
use std::sync::atomic::{AtomicBool, Ordering};
use vmm_sys_util::eventfd::{EFD_NONBLOCK, EventFd};
use zerocopy::IntoBytes;
fn frame_with_crc(msg_type: u32, payload: &[u8]) -> Vec<u8> {
let header = ShmMessage {
msg_type,
length: payload.len() as u32,
crc32: crc32fast::hash(payload),
_pad: 0,
};
let mut buf = Vec::with_capacity(FRAME_HEADER_SIZE + payload.len());
buf.extend_from_slice(header.as_bytes());
buf.extend_from_slice(payload);
buf
}
fn frame_with_torn_crc(msg_type: u32, payload: &[u8]) -> Vec<u8> {
let real_crc = crc32fast::hash(payload);
let header = ShmMessage {
msg_type,
length: payload.len() as u32,
crc32: real_crc ^ 0xFFFF_FFFF,
_pad: 0,
};
let mut buf = Vec::with_capacity(FRAME_HEADER_SIZE + payload.len());
buf.extend_from_slice(header.as_bytes());
buf.extend_from_slice(payload);
buf
}
fn snapshot_request_bytes(request_id: u32, kind: u32, tag: &str) -> Vec<u8> {
let tag_bytes = tag.as_bytes();
let mut tag_buf = [0u8; SNAPSHOT_TAG_MAX];
let n = tag_bytes.len().min(SNAPSHOT_TAG_MAX);
tag_buf[..n].copy_from_slice(&tag_bytes[..n]);
SnapshotRequestPayload {
request_id,
kind,
tag: tag_buf,
}
.as_bytes()
.to_vec()
}
fn run_sched_exit_gate(messages: &[crate::vmm::bulk::BulkMessage]) -> (bool, bool) {
let kill = AtomicBool::new(false);
let kill_evt = EventFd::new(EFD_NONBLOCK).expect("eventfd construction");
for msg in messages {
if msg.msg_type == MSG_TYPE_SCHED_EXIT && msg.crc_ok {
kill.store(true, Ordering::Release);
let _ = kill_evt.write(1);
}
}
let kill_value = kill.load(Ordering::Acquire);
let evt_fired = kill_evt.read().is_ok();
(kill_value, evt_fired)
}
fn run_snapshot_request_gate(messages: &[crate::vmm::bulk::BulkMessage]) -> usize {
let mut pending: Vec<SnapshotRequest> = Vec::new();
for msg in messages {
if msg.msg_type == MSG_TYPE_SNAPSHOT_REQUEST
&& msg.crc_ok
&& let Some(req) = decode_snapshot_request(&msg.payload[..])
{
pending.push(req);
}
}
pending.len()
}
#[test]
fn sched_exit_with_torn_crc_does_not_promote_kill() {
let mut a = HostAssembler::new();
let bytes = frame_with_torn_crc(MSG_TYPE_SCHED_EXIT, b"exit-payload");
let drained = a.feed(&bytes);
assert_eq!(drained.messages.len(), 1, "assembler emits one message");
assert!(
!drained.messages[0].crc_ok,
"torn CRC must surface as crc_ok=false"
);
assert_eq!(
drained.messages[0].msg_type, MSG_TYPE_SCHED_EXIT,
"msg_type unaffected by CRC mismatch — gate dispatch is by type"
);
let (kill, evt_fired) = run_sched_exit_gate(&drained.messages);
assert!(
!kill,
"kill flag must NOT flip on CRC-failed SCHED_EXIT — \
hostile guest must not force early exit"
);
assert!(
!evt_fired,
"kill eventfd must NOT be written on CRC-failed SCHED_EXIT — \
the BSP loop and watchdog must not be woken"
);
}
#[test]
fn sched_exit_with_valid_crc_does_promote_kill() {
let mut a = HostAssembler::new();
let bytes = frame_with_crc(MSG_TYPE_SCHED_EXIT, b"exit-payload");
let drained = a.feed(&bytes);
assert_eq!(drained.messages.len(), 1);
assert!(
drained.messages[0].crc_ok,
"matching CRC must surface as crc_ok=true"
);
let (kill, evt_fired) = run_sched_exit_gate(&drained.messages);
assert!(
kill,
"kill flag MUST flip on CRC-valid SCHED_EXIT — promotion is \
the load-bearing path that ends a test promptly"
);
assert!(
evt_fired,
"kill eventfd MUST be written on CRC-valid SCHED_EXIT — \
the BSP loop and watchdog need an epoll wake to exit \
the run loop"
);
}
#[test]
fn sched_exit_torn_crc_does_not_promote_when_other_valid_frames_present() {
let mut a = HostAssembler::new();
let mut buf = Vec::new();
buf.extend(frame_with_torn_crc(MSG_TYPE_SCHED_EXIT, b"first"));
buf.extend(frame_with_crc(
crate::vmm::wire::MSG_TYPE_STIMULUS,
b"valid",
));
buf.extend(frame_with_torn_crc(MSG_TYPE_SCHED_EXIT, b"second"));
let drained = a.feed(&buf);
assert_eq!(drained.messages.len(), 3);
assert!(!drained.messages[0].crc_ok);
assert!(drained.messages[1].crc_ok);
assert!(!drained.messages[2].crc_ok);
let (kill, evt_fired) = run_sched_exit_gate(&drained.messages);
assert!(
!kill,
"neither torn SCHED_EXIT may promote even though a CRC-valid \
non-SCHED_EXIT frame arrived alongside them"
);
assert!(!evt_fired, "kill eventfd must remain undisturbed");
}
#[test]
fn snapshot_request_with_torn_crc_is_dropped() {
let mut a = HostAssembler::new();
let payload = snapshot_request_bytes(7, SNAPSHOT_KIND_CAPTURE, "snap_dump");
let bytes = frame_with_torn_crc(MSG_TYPE_SNAPSHOT_REQUEST, &payload);
let drained = a.feed(&bytes);
assert_eq!(drained.messages.len(), 1, "assembler emits one message");
assert!(
!drained.messages[0].crc_ok,
"torn CRC must surface as crc_ok=false"
);
assert_eq!(
drained.messages[0].msg_type, MSG_TYPE_SNAPSHOT_REQUEST,
"msg_type unaffected by CRC mismatch"
);
let pushed = run_snapshot_request_gate(&drained.messages);
assert_eq!(
pushed, 0,
"CRC-failed SNAPSHOT_REQUEST must NOT decode — \
hostile guest must not force a capture or watchpoint arm"
);
}
#[test]
fn snapshot_request_with_valid_crc_is_pushed() {
let mut a = HostAssembler::new();
let payload = snapshot_request_bytes(42, SNAPSHOT_KIND_CAPTURE, "valid_tag");
let bytes = frame_with_crc(MSG_TYPE_SNAPSHOT_REQUEST, &payload);
let drained = a.feed(&bytes);
assert_eq!(drained.messages.len(), 1);
assert!(
drained.messages[0].crc_ok,
"matching CRC must surface as crc_ok=true"
);
let pushed = run_snapshot_request_gate(&drained.messages);
assert_eq!(
pushed, 1,
"CRC-valid well-formed SNAPSHOT_REQUEST MUST decode and push"
);
}
#[test]
fn snapshot_request_torn_crc_dropped_in_mixed_batch() {
let mut a = HostAssembler::new();
let p_first = snapshot_request_bytes(1, SNAPSHOT_KIND_CAPTURE, "first");
let p_torn = snapshot_request_bytes(2, SNAPSHOT_KIND_CAPTURE, "torn");
let p_third = snapshot_request_bytes(3, SNAPSHOT_KIND_CAPTURE, "third");
let mut buf = Vec::new();
buf.extend(frame_with_crc(MSG_TYPE_SNAPSHOT_REQUEST, &p_first));
buf.extend(frame_with_torn_crc(MSG_TYPE_SNAPSHOT_REQUEST, &p_torn));
buf.extend(frame_with_crc(MSG_TYPE_SNAPSHOT_REQUEST, &p_third));
let drained = a.feed(&buf);
assert_eq!(drained.messages.len(), 3);
assert!(drained.messages[0].crc_ok);
assert!(!drained.messages[1].crc_ok);
assert!(drained.messages[2].crc_ok);
let pushed = run_snapshot_request_gate(&drained.messages);
assert_eq!(
pushed, 2,
"exactly the two CRC-valid SNAPSHOT_REQUESTs must push; \
the torn middle frame must drop independently"
);
}
#[test]
fn both_gates_drop_torn_frames_in_same_drain() {
let mut a = HostAssembler::new();
let snap_payload = snapshot_request_bytes(99, SNAPSHOT_KIND_CAPTURE, "tag");
let mut buf = Vec::new();
buf.extend(frame_with_torn_crc(MSG_TYPE_SCHED_EXIT, b"sched-exit"));
buf.extend(frame_with_torn_crc(
MSG_TYPE_SNAPSHOT_REQUEST,
&snap_payload,
));
let drained = a.feed(&buf);
assert_eq!(drained.messages.len(), 2);
assert!(!drained.messages[0].crc_ok);
assert!(!drained.messages[1].crc_ok);
let (kill, evt_fired) = run_sched_exit_gate(&drained.messages);
let pushed = run_snapshot_request_gate(&drained.messages);
assert!(!kill, "torn SCHED_EXIT must not promote kill");
assert!(!evt_fired, "torn SCHED_EXIT must not write kill eventfd");
assert_eq!(pushed, 0, "torn SNAPSHOT_REQUEST must not decode");
}
fn run_sys_rdy_gate(messages: &[crate::vmm::bulk::BulkMessage]) -> (u32, bool) {
let evt = std::sync::Arc::new(EventFd::new(EFD_NONBLOCK).expect("eventfd construction"));
let mut sys_rdy_evt: Option<std::sync::Arc<EventFd>> = Some(evt.clone());
for msg in messages {
if msg.msg_type == MSG_TYPE_SYS_RDY
&& msg.crc_ok
&& let Some(evt) = sys_rdy_evt.take()
{
let _ = evt.write(1);
}
}
let remaining = sys_rdy_evt.is_some();
let counter = match evt.read() {
Ok(n) => n as u32,
Err(_) => 0,
};
(counter, remaining)
}
#[test]
fn sys_rdy_with_torn_crc_does_not_fire_eventfd() {
let mut a = HostAssembler::new();
let bytes = frame_with_torn_crc(MSG_TYPE_SYS_RDY, b"");
let drained = a.feed(&bytes);
assert_eq!(drained.messages.len(), 1, "assembler emits one message");
assert!(
!drained.messages[0].crc_ok,
"torn CRC must surface as crc_ok=false"
);
assert_eq!(
drained.messages[0].msg_type, MSG_TYPE_SYS_RDY,
"msg_type unaffected by CRC mismatch"
);
let (counter, remaining) = run_sys_rdy_gate(&drained.messages);
assert_eq!(
counter, 0,
"boot-complete eventfd must NOT be written on CRC-failed \
SYS_RDY — hostile guest must not race ahead of percpu/KASLR"
);
assert!(
remaining,
"Option::take must NOT consume the handle on a dropped frame — \
a later CRC-valid SYS_RDY must still be able to promote"
);
}
#[test]
fn sys_rdy_with_valid_crc_fires_eventfd_once() {
let mut a = HostAssembler::new();
let bytes = frame_with_crc(MSG_TYPE_SYS_RDY, b"");
let drained = a.feed(&bytes);
assert_eq!(drained.messages.len(), 1);
assert!(
drained.messages[0].crc_ok,
"matching CRC must surface as crc_ok=true"
);
let (counter, remaining) = run_sys_rdy_gate(&drained.messages);
assert_eq!(
counter, 1,
"boot-complete eventfd MUST receive a single write on \
CRC-valid SYS_RDY"
);
assert!(
!remaining,
"Option::take must consume the handle so subsequent \
SYS_RDY frames do not pump the counter"
);
}
#[test]
fn sys_rdy_with_valid_crc_fires_once_then_subsequent_drops() {
let mut a = HostAssembler::new();
let mut buf = Vec::new();
buf.extend(frame_with_crc(MSG_TYPE_SYS_RDY, b""));
buf.extend(frame_with_crc(MSG_TYPE_SYS_RDY, b""));
let drained = a.feed(&buf);
assert_eq!(drained.messages.len(), 2);
assert!(drained.messages[0].crc_ok);
assert!(drained.messages[1].crc_ok);
let (counter, remaining) = run_sys_rdy_gate(&drained.messages);
assert_eq!(
counter, 1,
"second SYS_RDY must NOT pump the eventfd — \
Option::take consumed the handle on the first promotion"
);
assert!(!remaining);
}
#[test]
fn sys_rdy_and_sched_exit_fire_independently() {
let mut a = HostAssembler::new();
let mut buf = Vec::new();
buf.extend(frame_with_crc(MSG_TYPE_SYS_RDY, b""));
buf.extend(frame_with_crc(MSG_TYPE_SCHED_EXIT, b"exit-payload"));
let drained = a.feed(&buf);
assert_eq!(drained.messages.len(), 2);
assert!(drained.messages[0].crc_ok);
assert!(drained.messages[1].crc_ok);
let (rdy_counter, rdy_remaining) = run_sys_rdy_gate(&drained.messages);
let (kill, kill_evt_fired) = run_sched_exit_gate(&drained.messages);
assert_eq!(rdy_counter, 1, "SYS_RDY must promote");
assert!(!rdy_remaining, "SYS_RDY handle must be consumed");
assert!(kill, "SCHED_EXIT must promote kill");
assert!(kill_evt_fired, "SCHED_EXIT must write kill eventfd");
}