use super::*;
#[test]
fn vcpu_reg_snapshot_display_renders_three_hex_fields() {
let s = VcpuRegSnapshot {
instruction_pointer: 0xffff_ffff_8100_1234,
stack_pointer: 0xffff_ffff_8000_0000,
page_table_root: 0x0123_4567_89ab_cdef,
user_page_table_root: None,
tcr_el1: None,
};
let out = format!("{s}");
assert_eq!(
out,
"ip=0xffffffff81001234 sp=0xffffffff80000000 ptroot=0x0123456789abcdef"
);
}
#[test]
fn vcpu_reg_snapshot_display_appends_user_pt_root_when_present() {
let s = VcpuRegSnapshot {
instruction_pointer: 0xffff_8000_8100_1234,
stack_pointer: 0xffff_8000_8000_0000,
page_table_root: 0x0000_4000_8000_0000,
user_page_table_root: Some(0x0000_0000_aaaa_bbbb),
tcr_el1: Some(0xb510_0010),
};
let out = format!("{s}");
assert_eq!(
out,
"ip=0xffff800081001234 sp=0xffff800080000000 ptroot=0x0000400080000000 uptroot=0x00000000aaaabbbb"
);
}
#[test]
fn vcpu_reg_snapshot_serde_round_trip() {
let s = VcpuRegSnapshot {
instruction_pointer: 0x1,
stack_pointer: 0x2,
page_table_root: 0x3,
user_page_table_root: None,
tcr_el1: None,
};
let json = serde_json::to_string(&s).expect("serialize");
assert!(
json.contains("\"instruction_pointer\""),
"missing JSON key `instruction_pointer`: {json}"
);
assert!(
json.contains("\"stack_pointer\""),
"missing JSON key `stack_pointer`: {json}"
);
assert!(
json.contains("\"page_table_root\""),
"missing JSON key `page_table_root`: {json}"
);
assert!(
!json.contains("\"user_page_table_root\""),
"user_page_table_root must skip-serialize when None: {json}"
);
let parsed: VcpuRegSnapshot = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed.instruction_pointer, 0x1);
assert_eq!(parsed.stack_pointer, 0x2);
assert_eq!(parsed.page_table_root, 0x3);
assert!(
parsed.user_page_table_root.is_none(),
"missing field must deserialize as None"
);
}
#[test]
fn vcpu_reg_snapshot_serde_round_trip_with_user_pt_root() {
let s = VcpuRegSnapshot {
instruction_pointer: 0x1,
stack_pointer: 0x2,
page_table_root: 0x3,
user_page_table_root: Some(0xdead_beef_cafe_d00d),
tcr_el1: None,
};
let json = serde_json::to_string(&s).expect("serialize");
assert!(
json.contains("\"user_page_table_root\""),
"user_page_table_root must serialize when Some: {json}"
);
let parsed: VcpuRegSnapshot = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed.user_page_table_root, Some(0xdead_beef_cafe_d00d));
}
#[test]
fn vcpu_reg_snapshot_serde_round_trip_tcr_el1() {
let some_val: u64 = 0x0000_0000_b510_0010;
let s_some = VcpuRegSnapshot {
instruction_pointer: 0x1,
stack_pointer: 0x2,
page_table_root: 0x3,
user_page_table_root: None,
tcr_el1: Some(some_val),
};
let json_some = serde_json::to_string(&s_some).expect("serialize Some");
assert!(
json_some.contains("\"tcr_el1\""),
"tcr_el1 must serialize when Some: {json_some}"
);
let s_none = VcpuRegSnapshot {
instruction_pointer: 0x1,
stack_pointer: 0x2,
page_table_root: 0x3,
user_page_table_root: None,
tcr_el1: None,
};
let json_none = serde_json::to_string(&s_none).expect("serialize None");
assert!(
!json_none.contains("\"tcr_el1\""),
"tcr_el1 must skip-serialize when None: {json_none}"
);
let parsed_some: VcpuRegSnapshot = serde_json::from_str(&json_some).expect("deserialize Some");
assert_eq!(parsed_some.tcr_el1, Some(some_val));
let parsed_none: VcpuRegSnapshot = serde_json::from_str(&json_none).expect("deserialize None");
assert!(
parsed_none.tcr_el1.is_none(),
"missing tcr_el1 must deserialize as None"
);
}
#[test]
fn vcpu_reg_snapshot_zero_renders_zeros() {
let s = VcpuRegSnapshot {
instruction_pointer: 0,
stack_pointer: 0,
page_table_root: 0,
user_page_table_root: None,
tcr_el1: None,
};
assert_eq!(
format!("{s}"),
"ip=0x0000000000000000 sp=0x0000000000000000 ptroot=0x0000000000000000"
);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn aarch64_register_ids_match_kernel_encoding() {
const KVM_REG_ARM64: u64 = 0x6000_0000_0000_0000;
const KVM_REG_SIZE_U64: u64 = 0x0030_0000_0000_0000;
const KVM_REG_ARM_CORE: u64 = 0x0010_0000;
const KVM_REG_ARM64_SYSREG: u64 = 0x0013_0000;
const EXPECTED_PC_ID: u64 = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE | 64;
const EXPECTED_SP_EL1_ID: u64 = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE | 68;
const EXPECTED_TTBR0_EL1_ID: u64 =
KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM64_SYSREG | 0xC100;
const EXPECTED_TTBR1_EL1_ID: u64 =
KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM64_SYSREG | 0xC101;
let pc_id = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE | (256 / 4);
let sp_el1_id = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE | (272 / 4);
let ttbr0_el1_id = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM64_SYSREG | 0xC100;
let ttbr1_el1_id = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM64_SYSREG | 0xC101;
assert_eq!(pc_id, EXPECTED_PC_ID, "PC_ID encoding drift");
assert_eq!(
sp_el1_id, EXPECTED_SP_EL1_ID,
"SP_EL1_ID encoding drift — note offset is 272 (sp_el1), \
not 248 (sp_el0)"
);
assert_eq!(
ttbr0_el1_id, EXPECTED_TTBR0_EL1_ID,
"TTBR0_EL1_ID encoding drift — verify (Op0=3, Op1=0, \
CRn=2, CRm=0, Op2=0) packs to 0xC100"
);
assert_eq!(
ttbr1_el1_id, EXPECTED_TTBR1_EL1_ID,
"TTBR1_EL1_ID encoding drift — verify (Op0=3, Op1=0, \
CRn=2, CRm=0, Op2=1) packs to 0xC101"
);
assert_eq!(
ttbr1_el1_id - ttbr0_el1_id,
1,
"TTBR0/TTBR1 encodings should differ by exactly 1 (Op2 bit)"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn dr7_slot0_write_4byte_encoding() {
const DR7_FIXED_1: u64 = 1 << 10;
const DR_LOCAL_EXACT: u64 = 1 << 8; const DR_GLOBAL_EXACT: u64 = 1 << 9; const DR_LOCAL_ENABLE: u64 = 1 << 0; const DR_GLOBAL_ENABLE: u64 = 1 << 1; const DR_RW_WRITE: u64 = 0b01;
const DR_LEN_4: u64 = 0b11;
const SLOT0_RW_SHIFT: u32 = 16;
const SLOT0_LEN_SHIFT: u32 = 18;
let dr7 = DR7_FIXED_1
| DR_GLOBAL_EXACT
| DR_LOCAL_EXACT
| DR_LOCAL_ENABLE
| DR_GLOBAL_ENABLE
| (DR_RW_WRITE << SLOT0_RW_SHIFT)
| (DR_LEN_4 << SLOT0_LEN_SHIFT);
assert_eq!(
dr7, 0xD0703,
"DR7 encoding for (slot=0, write, 4B) must match the production wire format"
);
assert_ne!(dr7 & (1 << 0), 0, "L0 (bit 0) must be set");
assert_ne!(dr7 & (1 << 1), 0, "G0 (bit 1) must be set");
assert_ne!(
dr7 & (1 << 8),
0,
"LE (bit 8) must be set for data breakpoints"
);
assert_ne!(
dr7 & (1 << 9),
0,
"GE (bit 9) must be set for data breakpoints"
);
assert_ne!(dr7 & (1 << 10), 0, "DR7_FIXED_1 (bit 10) must be set");
assert_eq!(
(dr7 >> SLOT0_RW_SHIFT) & 0b11,
DR_RW_WRITE,
"slot 0 R/W field must encode write (0b01)"
);
assert_eq!(
(dr7 >> SLOT0_LEN_SHIFT) & 0b11,
DR_LEN_4,
"slot 0 LEN field must encode 4 bytes (0b11)"
);
assert_eq!(
dr7 & 0b1111_1100,
0,
"slots 1..3 must be disabled (L/G bits clear)"
);
assert_eq!(
(dr7 >> 20) & 0xFFF,
0,
"slots 1..3 R/W + LEN fields must be zero"
);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn dbgwcr_slot0_write_4byte_encoding_offset0() {
let kva: u64 = 0xffff_ffff_8100_1000; let byte_offset = (kva & 0x7u64) as u32;
let bas: u64 = 0xFu64 << byte_offset;
let wcr: u64 = 1u64 | (0b11u64 << 1) | (0b10u64 << 3) | (bas << 5);
assert_eq!(
wcr, 0x1F7,
"DBGWCR encoding for (slot=0, write, 4B, offset=0) must \
match the QEMU/ARM ARM gold-standard wire format"
);
assert_eq!(wcr & 1, 1, "E (bit 0) must be set");
assert_eq!(
(wcr >> 1) & 0b11,
0b11,
"PAC (bits 2:1) must be 0b11 (EL0+EL1)"
);
assert_eq!(
(wcr >> 3) & 0b11,
0b10,
"LSC (bits 4:3) must be 0b10 (write-only)"
);
assert_eq!(
(wcr >> 5) & 0xFF,
0x0F,
"BAS (bits 12:5) must be 0x0F for offset=0 (4 \
contiguous low bytes)"
);
assert_eq!(
(wcr >> 13) & 0xF,
0,
"HMC + low SSC bit (bits 16:13) must be zero"
);
assert_eq!((wcr >> 20) & 0xF, 0, "WT + LBN must be zero");
assert_eq!((wcr >> 24) & 0x1F, 0, "MASK must be zero");
}
#[test]
#[cfg(target_arch = "aarch64")]
fn dbgwcr_slot0_write_4byte_encoding_offset4() {
let kva: u64 = 0xffff_ffff_8100_1004; let byte_offset = (kva & 0x7u64) as u32;
let bas: u64 = 0xFu64 << byte_offset;
let wcr: u64 = 1u64 | (0b11u64 << 1) | (0b10u64 << 3) | (bas << 5);
assert_eq!(
wcr, 0x1E17,
"DBGWCR encoding for (slot=0, write, 4B, offset=4) must \
match `0x1 | (3<<1) | (2<<3) | (0xF0 << 5)` = 0x1E17"
);
assert_eq!(
(wcr >> 5) & 0xFF,
0xF0,
"BAS (bits 12:5) must be 0xF0 for offset=4 (4 \
contiguous high bytes)"
);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn dbgwvr_8byte_aligned_base() {
let kva: u64 = 0xffff_ffff_8100_1004;
let wvr = kva & !0x7u64;
assert_eq!(
wvr, 0xffff_ffff_8100_1000,
"DBGWVR base must clear the bottom 3 bits (8-byte align) \
so BAS picks the 4 watched bytes within the block"
);
let byte_offset = (kva & 0x7u64) as u32;
let bas: u64 = 0xFu64 << byte_offset;
let watched_lo = wvr + (bas.trailing_zeros() as u64);
let watched_hi = watched_lo + (bas.count_ones() as u64);
assert_eq!(
watched_lo, kva,
"watched range low must equal the original KVA"
);
assert_eq!(
watched_hi,
kva + 4,
"watched range high must equal kva + 4 (4 bytes)"
);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn watchpoint_slot_decode_from_far_user_slot() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
let armed_slots: [u64; 4] = [
0,
0xffff_ffff_8100_1000,
0xffff_ffff_8100_1004,
0xffff_ffff_8100_1008,
];
let far = 0xffff_ffff_8100_1004u64;
let hsr = (super::ESR_ELX_EC_WATCHPT_LOW) << super::ESR_ELX_EC_SHIFT;
let debug_arch = kvm_bindings::kvm_debug_exit_arch {
hsr,
hsr_high: 0,
far,
};
let mut single_step_pending = false;
let mut single_step_slot: usize = 99;
super::dispatch_watchpoint_hit(
&watchpoint,
&debug_arch,
&armed_slots,
&mut single_step_pending,
&mut single_step_slot,
);
assert!(
!watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"slot 0 (exit_kind) must not latch when a different \
slot's range matches FAR"
);
assert!(
!watchpoint.user[0]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[0] / slot 1 must not latch — FAR is inside slot 2's \
range"
);
assert!(
watchpoint.user[1]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[1] / slot 2 must latch — FAR equals the slot's KVA"
);
assert!(
!watchpoint.user[2]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[2] / slot 3 must not latch — FAR is outside its \
range"
);
assert!(
single_step_pending,
"single_step_pending must be set when a watchpoint match \
latches; without this the next KVM_RUN replays the same \
store and re-trips the watchpoint forever (ARM ARM \
D2.10.5)"
);
assert_eq!(
single_step_slot, 0b0100,
"single_step_slot bitmap must encode slot 2 (bit 2 = 1, \
0b0100) so self_arm_watchpoint clears WCR[2].E and \
leaves WCR[0/1/3].E armed for the single-step pass"
);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn watchpoint_dispatch_ignores_non_watchpt_ec() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
let armed_slots: [u64; 4] = [
0xffff_ffff_8100_1000,
0xffff_ffff_8100_1004,
0xffff_ffff_8100_1008,
0xffff_ffff_8100_100c,
];
let hsr = 0x32u32 << super::ESR_ELX_EC_SHIFT;
let debug_arch = kvm_bindings::kvm_debug_exit_arch {
hsr,
hsr_high: 0,
far: 0xffff_ffff_8100_1004,
};
let mut single_step_pending = false;
let mut single_step_slot: usize = 99;
super::dispatch_watchpoint_hit(
&watchpoint,
&debug_arch,
&armed_slots,
&mut single_step_pending,
&mut single_step_slot,
);
assert!(
!watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"soft-step EC must not latch slot 0"
);
for (i, slot) in watchpoint.user.iter().enumerate() {
assert!(
!slot.hit.load(std::sync::atomic::Ordering::Acquire),
"soft-step EC must not latch user[{i}]"
);
}
assert!(
!single_step_pending,
"spurious soft-step exit (no pending step) must leave \
single_step_pending unchanged"
);
assert_eq!(
single_step_slot, 99,
"spurious soft-step exit must not clobber single_step_slot"
);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn watchpoint_softstep_clears_single_step_pending() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
let armed_slots: [u64; 4] = [0, 0xffff_ffff_8100_1000, 0, 0];
let hsr = super::ESR_ELX_EC_SOFTSTP_LOW << super::ESR_ELX_EC_SHIFT;
let debug_arch = kvm_bindings::kvm_debug_exit_arch {
hsr,
hsr_high: 0,
far: 0,
};
let mut single_step_pending = true;
let mut single_step_slot: usize = 1;
super::dispatch_watchpoint_hit(
&watchpoint,
&debug_arch,
&armed_slots,
&mut single_step_pending,
&mut single_step_slot,
);
assert!(
!single_step_pending,
"SOFTSTP_LOW with pending step must clear \
single_step_pending so the next self_arm_watchpoint \
call restores WCR.E=1 and drops KVM_GUESTDBG_SINGLESTEP"
);
assert!(
!watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"SOFTSTP_LOW must not latch slot 0 (the WATCHPT_LOW \
exit that preceded it already did)"
);
for (i, slot) in watchpoint.user.iter().enumerate() {
assert!(
!slot.hit.load(std::sync::atomic::Ordering::Acquire),
"SOFTSTP_LOW must not latch user[{i}]"
);
}
}
#[test]
#[cfg(target_arch = "aarch64")]
fn watchpoint_slot0_skips_latch_when_host_ptr_null() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
let armed_slots: [u64; 4] = [0xffff_ffff_8100_1000, 0, 0, 0];
let hsr = (super::ESR_ELX_EC_WATCHPT_LOW) << super::ESR_ELX_EC_SHIFT;
let debug_arch = kvm_bindings::kvm_debug_exit_arch {
hsr,
hsr_high: 0,
far: 0xffff_ffff_8100_1000,
};
let mut single_step_pending = false;
let mut single_step_slot: usize = 0;
super::dispatch_watchpoint_hit(
&watchpoint,
&debug_arch,
&armed_slots,
&mut single_step_pending,
&mut single_step_slot,
);
assert!(
!watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"slot 0 must NOT latch hit when kind_host_ptr is null — \
a null observation is a publication-invariant violation, \
not a fallback trigger"
);
assert!(
single_step_pending,
"single_step_pending must be set when the FAR matches a \
slot, regardless of slot-0 latch outcome"
);
assert_eq!(
single_step_slot, 0b0001,
"single_step_slot bitmap must include slot 0 (bit 0 = 1) \
so self_arm_watchpoint clears WCR[0].E during the \
single-step pass"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn watchpoint_dispatch_x86_dr6_b2_latches_user_slot_1() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
let armed_slots: [u64; 4] = [0; 4];
let debug_arch = kvm_bindings::kvm_debug_exit_arch {
exception: 0,
pad: 0,
pc: 0,
dr6: 0x4,
dr7: 0,
};
let mut single_step_pending = false;
let mut single_step_slot: usize = 99;
super::dispatch_watchpoint_hit(
&watchpoint,
&debug_arch,
&armed_slots,
&mut single_step_pending,
&mut single_step_slot,
);
assert!(
!watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"slot 0 (exit_kind) must not latch when DR6 B0 is clear"
);
assert!(
!watchpoint.user[0]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[0] / slot 1 must not latch — DR6 B1 is clear"
);
assert!(
watchpoint.user[1]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[1] / slot 2 must latch — DR6 B2 is set"
);
assert!(
!watchpoint.user[2]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[2] / slot 3 must not latch — DR6 B3 is clear"
);
assert!(
!single_step_pending,
"x86 dispatch must never set single_step_pending — \
single-step is aarch64-only"
);
assert_eq!(
single_step_slot, 99,
"x86 dispatch must not clobber single_step_slot — \
single-step is aarch64-only"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn watchpoint_dispatch_x86_dr6_multi_match() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
let kind: u32 = super::SCX_EXIT_ERROR_THRESHOLD;
let kind_box = Box::new(kind);
let kind_ptr = Box::into_raw(kind_box);
watchpoint
.kind_host_ptr
.store(kind_ptr, std::sync::atomic::Ordering::Release);
let armed_slots: [u64; 4] = [0; 4];
let debug_arch = kvm_bindings::kvm_debug_exit_arch {
exception: 0,
pad: 0,
pc: 0,
dr6: 0x5,
dr7: 0,
};
let mut single_step_pending = false;
let mut single_step_slot: usize = 99;
super::dispatch_watchpoint_hit(
&watchpoint,
&debug_arch,
&armed_slots,
&mut single_step_pending,
&mut single_step_slot,
);
assert!(
watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"slot 0 (exit_kind) must latch — DR6 B0 set + kind ≥ \
SCX_EXIT_ERROR_THRESHOLD"
);
assert!(
!watchpoint.user[0]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[0] / slot 1 must not latch — DR6 B1 is clear"
);
assert!(
watchpoint.user[1]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[1] / slot 2 must latch — DR6 B2 is set, even \
though slot 0 latched first in iteration order"
);
assert!(
!watchpoint.user[2]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"user[2] / slot 3 must not latch — DR6 B3 is clear"
);
let _ = unsafe { Box::from_raw(kind_ptr) };
}
#[test]
fn latch_hit_is_idempotent_across_repeat_calls() {
use crate::vmm::vcpu::WatchpointArm;
use std::os::fd::AsRawFd;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
watchpoint.latch_hit();
assert!(
watchpoint.hit.load(std::sync::atomic::Ordering::Acquire),
"first latch_hit must flip hit=false→true"
);
let mut buf = [0u8; 8];
let n = unsafe {
libc::read(
watchpoint.hit_evt.as_raw_fd(),
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
assert_eq!(
n, 8,
"first latch_hit must produce one eventfd edge \
(8-byte counter read)"
);
let count_after_first = u64::from_ne_bytes(buf);
assert_eq!(
count_after_first, 1,
"first latch_hit must increment counter by exactly 1"
);
watchpoint.latch_hit();
let mut buf2 = [0u8; 8];
let n2 = unsafe {
libc::read(
watchpoint.hit_evt.as_raw_fd(),
buf2.as_mut_ptr() as *mut libc::c_void,
buf2.len(),
)
};
let errno = unsafe { *libc::__errno_location() };
assert!(
n2 == -1 && errno == libc::EAGAIN,
"second latch_hit on already-latched slot must NOT \
write to hit_evt (cross-vCPU dedup); read should \
return EAGAIN, got n={n2} errno={errno}"
);
}
#[test]
fn latch_user_hit_is_idempotent_across_repeat_calls() {
use crate::vmm::vcpu::WatchpointArm;
use std::os::fd::AsRawFd;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
watchpoint.latch_user_hit(1);
assert!(
watchpoint.user[1]
.hit
.load(std::sync::atomic::Ordering::Acquire),
"first latch_user_hit(1) must flip user[1].hit=false→true"
);
let mut buf = [0u8; 8];
let n = unsafe {
libc::read(
watchpoint.hit_evt.as_raw_fd(),
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
assert_eq!(n, 8, "first latch_user_hit(1) must write eventfd");
let count_after_first = u64::from_ne_bytes(buf);
assert_eq!(count_after_first, 1, "counter increment must be 1");
watchpoint.latch_user_hit(1);
let mut buf2 = [0u8; 8];
let n2 = unsafe {
libc::read(
watchpoint.hit_evt.as_raw_fd(),
buf2.as_mut_ptr() as *mut libc::c_void,
buf2.len(),
)
};
let errno = unsafe { *libc::__errno_location() };
assert!(
n2 == -1 && errno == libc::EAGAIN,
"second latch_user_hit(1) on already-latched slot \
must NOT write to hit_evt; read should return \
EAGAIN, got n={n2} errno={errno}"
);
watchpoint.latch_user_hit(99);
for (i, slot) in watchpoint.user.iter().enumerate() {
if i == 1 {
assert!(
slot.hit.load(std::sync::atomic::Ordering::Acquire),
"user[1].hit must remain latched"
);
} else {
assert!(
!slot.hit.load(std::sync::atomic::Ordering::Acquire),
"user[{i}].hit must remain unlatched after \
out-of-range latch_user_hit(99)"
);
}
}
}
#[test]
fn mark_armed_flips_gate_and_is_idempotent() {
use crate::vmm::vcpu::WatchpointArm;
let watchpoint = WatchpointArm::new().expect("WatchpointArm::new");
assert_eq!(
watchpoint
.any_armed
.load(std::sync::atomic::Ordering::Relaxed),
0,
"newly-constructed WatchpointArm must have any_armed=0 \
so self_arm_watchpoint short-circuits before any \
publisher fires"
);
watchpoint.mark_armed();
assert_eq!(
watchpoint
.any_armed
.load(std::sync::atomic::Ordering::Relaxed),
1,
"first mark_armed call must flip the gate to 1"
);
watchpoint.mark_armed();
assert_eq!(
watchpoint
.any_armed
.load(std::sync::atomic::Ordering::Relaxed),
1,
"second mark_armed call must leave the gate at 1 \
(idempotent — mark_armed is `store(1)`, not `+= 1`)"
);
}