From 5446f7283cd88e64243137b428f81b887ef1a9fa Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Mon, 12 Oct 2026 10:45:45 +0000 Subject: [PATCH 1/2] test: compare VirtualBox access methods live --- doc/src/developer/tests.md | 47 ++++- tests/tests/virtualbox.rs | 397 +++++++++++++++++++++++++++++++++++++ 2 files changed, 442 insertions(+), 2 deletions(-) diff --git a/doc/src/developer/tests.md b/doc/src/developer/tests.md index 8f578030..4b168f3d 100644 --- a/doc/src/developer/tests.md +++ b/doc/src/developer/tests.md @@ -40,8 +40,9 @@ in a fresh core and survive a resume/re-pause cycle. The generic `get_vcpu_count configured `cpus`. **Requirements** -- a VirtualBox build with the MachineDebugger patches (MachineDebugger method) or with VBoxVMI - (VBoxVMI method), its kernel modules loaded +- a VirtualBox build with the MachineDebugger patches (MachineDebugger method), VBoxVMI + (VBoxVMI method), or both patch series on a test-only combined branch (cross-method test), with + that build's kernel modules loaded - a VM with an online snapshot (saved state), e.g. a live Alpine ISO guest at its login prompt **Configuration** (environment variables) @@ -89,6 +90,48 @@ second client. The other filters cover `pause`, `virtualbox_vcpu_count_matches_vmm`. With `VBOX_TEST_CPUS=2`, the register tests read every vCPU and write the last one. +### MachineDebugger/VBoxVMI cross-method consistency + +The `virtualbox_cross_method_consistency` inventory test opens MachineDebugger and VBoxVMI +concurrently against the same VM process, pauses it through MachineDebugger, and compares every +register on every vCPU and every byte in the live PGM RAM ranges directly between the two methods. +It checks both drivers' maximum physical address against the end of those ranges, reports the exact +byte count above 4 GiB, and uses no core dump. It then writes the same safe zero-page locations used +by the other VirtualBox write test and the last vCPU's `SYSENTER_ESP`/`SYSENTER_EIP` in both +directions. Every write is read independently through the other method. Originals are restored and +verified through both methods before resume; an unwind guard repeats restoration, and the standard +teardown powers off and restores the base snapshot. + +One VirtualBox process must implement both APIs. The long-lived `vmi/machinedebugger` and +`vmi/vboxvmi` branches intentionally stay separate; use the test-only +`Wenzel/virtualbox` branch `vmi/cross-method-test`, which merges the completed MachineDebugger +patches into `vmi/vboxvmi`: + +~~~ +git -C /path/to/virtualbox fetch fork +git -C /path/to/virtualbox worktree add /path/to/virtualbox-vmi fork/vmi/cross-method-test +/path/to/vbox-dev/build.sh /path/to/virtualbox-vmi +~~~ + +Compile the integration binary with both access methods. Run it on the host with +`VBOX_APP_HOME` pointing at that same combined build: this is required both for the VM's VBoxVMI +server and for MachineDebugger's dynamically loaded `VBoxC.so`. `VBOX_TEST_METHOD=vboxvmi` makes +the context enable the server and supplies its one client to the cross-method test. The test +requires two vCPUs. Run both the 512 MiB case and the 5120 MiB case, which must report nonzero bytes +and a selected write page above 4 GiB: + +~~~ +cargo test --features 'virtualbox-api virtualbox-vmi' --test integration --no-run +BIN=target/debug/deps/integration- +COMMON='VBOX_APP_HOME=/path/to/virtualbox-vmi/out/linux.amd64/release/bin VBOX_USER_HOME=/path/to/vbox-home VBOX_TEST_METHOD=vboxvmi VBOX_TEST_CPUS=2 MICROVMI_TEST_FILTER=cross_method_consistency MICROVMI_TEST_TIMEOUT=180 VBOX_TEST_CORE_DIR=/path/to/disk-backed/core-dir' +flock -o /path/to/vbox-dev/.vm.lock env $COMMON VBOX_TEST_MEMORY_MB=512 $BIN +flock -o /path/to/vbox-dev/.vm.lock env $COMMON VBOX_TEST_MEMORY_MB=5120 $BIN +~~~ + +Run only a binary built from the matching libmicrovmi checkout. Teardown runs after setup or test +failure; do not interrupt teardown. A completed run leaves the VM powered off with the base +snapshot restored. + If cargo runs in a container without VirtualBox, build the test binary there and run it on the host: ~~~ diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index 0dcc5d05..e92f4e41 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -1048,3 +1048,400 @@ inventory::submit!(IntegrationTest { name: "virtualbox_write_registers_survive_resume", test_fn: write_registers_survive_resume }); + +/// Both VirtualBox access methods attached to one VM, compared without a core dump. +#[cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] +mod cross_method { + use super::*; + use microvmi::api::params::{CommonInitParams, DriverInitParams, VBoxInitParams}; + use microvmi::api::DriverType; + + fn read_exact(drv: &dyn Introspectable, method: &str, addr: u64, buf: &mut [u8]) { + let mut bytes_read = 0; + drv.read_physical(addr, buf, &mut bytes_read) + .unwrap_or_else(|error| { + panic!( + "{} failed to read RAM at {:#x} after {} bytes: {}", + method, addr, bytes_read, error + ) + }); + assert_eq!( + bytes_read, + buf.len() as u64, + "{} short read at {:#x}", + method, + addr + ); + } + + fn read_page(drv: &dyn Introspectable, method: &str, addr: u64) -> Vec { + let mut page = vec![0; PAGE as usize]; + read_exact(drv, method, addr, &mut page); + page + } + + fn assert_page(drv: &dyn Introspectable, method: &str, addr: u64, expected: &[u8]) { + let actual = read_page(drv, method, addr); + if let Some(offset) = actual + .iter() + .zip(expected) + .position(|(actual, expected)| actual != expected) + { + panic!( + "{} RAM differs at {:#x}: {:#04x} != {:#04x}", + method, + addr + offset as u64, + actual[offset], + expected[offset] + ); + } + } + + fn assert_registers( + drv: &dyn Introspectable, + method: &str, + vcpu: u16, + expected: &X86Registers, + ) { + assert_eq!( + x86_registers(drv, vcpu), + *expected, + "{} registers differ on vCPU {}", + method, + vcpu + ); + } + + /// The same safe page choices as the core-based write test, found directly through both live + /// methods: the first all-zero page below low reserved RAM, mid-RAM, low RAM and top RAM. + fn zero_pages_live( + machine_debugger: &dyn Introspectable, + vboxvmi: &dyn Introspectable, + ranges: &[Range], + ) -> Vec { + let top = ranges.iter().map(|range| range.end).max().unwrap(); + let top_below_4g = ranges + .iter() + .map(|range| range.end) + .filter(|&end| end <= FOUR_GIB) + .max() + .unwrap(); + let mut pages: Vec = [0x10000, top / 2, top_below_4g, top] + .iter() + .map(|&target| { + let range = ranges + .iter() + .find(|range| range.start < target && target <= range.end) + .unwrap_or_else(|| panic!("{:#x} is not in a RAM range", target)); + (1..=1024) + .filter_map(|i| (target & !(PAGE - 1)).checked_sub(i * PAGE)) + .take_while(|&page| page >= range.start.max(PAGE)) + .find(|&page| { + let md = read_page(machine_debugger, "MachineDebugger", page); + let vmi = read_page(vboxvmi, "VBoxVMI", page); + assert_eq!(md, vmi, "methods differ at candidate page {:#x}", page); + md.iter().all(|&byte| byte == 0) + }) + .unwrap_or_else(|| panic!("no zero page below {:#x}", target)) + }) + .collect(); + pages.sort_unstable(); + pages.dedup(); + pages + } + + fn pattern(addr: u64, salt: u64) -> Vec { + let mut state = (addr ^ salt) | 1; + (0..PAGE) + .map(|_| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state as u8 + }) + .collect() + } + + /// Restores through both methods before `ResumeVm` resumes, including during unwinding. The + /// outer VirtualBox context then powers off and restores the base snapshot. + struct CrossMethodRestore<'a> { + machine_debugger: &'a dyn Introspectable, + vboxvmi: &'a dyn Introspectable, + pages: Vec<(u64, Vec)>, + vcpu: u16, + registers: X86Registers, + armed: bool, + } + + impl CrossMethodRestore<'_> { + fn restore_with(&self, drv: &dyn Introspectable, method: &str) { + for (addr, original) in &self.pages { + if let Err(error) = drv.write_physical(*addr, original) { + eprintln!("{} failed to restore RAM at {:#x}: {}", method, addr, error); + } + } + if let Err(error) = + drv.write_registers(self.vcpu, Registers::X86(self.registers.clone())) + { + eprintln!( + "{} failed to restore vCPU {} registers: {}", + method, self.vcpu, error + ); + } + } + } + + impl Drop for CrossMethodRestore<'_> { + fn drop(&mut self) { + if self.armed { + let _ = try_vboxmanage(&["controlvm", &vm_name(), "pause"]); + self.restore_with(self.machine_debugger, "MachineDebugger"); + self.restore_with(self.vboxvmi, "VBoxVMI"); + } + } + } + + fn init_machine_debugger() -> Box { + microvmi::init( + Some(DriverType::VirtualBox), + Some(DriverInitParams { + common: Some(CommonInitParams { vm_name: vm_name() }), + virtualbox: Some(VBoxInitParams::MachineDebugger), + ..Default::default() + }), + ) + .expect("failed to attach MachineDebugger") + } + + /// On one paused VM, compare every register of every vCPU and every byte of all PGM RAM ranges + /// directly between the methods. Then write safe RAM pages and SYSENTER registers in both + /// directions, with independent readback and exact restoration through both methods. + fn consistency(vboxvmi: Box) { + assert!( + matches!(env::var("VBOX_TEST_METHOD").as_deref(), Ok("vboxvmi")), + "cross-method test requires VBOX_TEST_METHOD=vboxvmi" + ); + let mut machine_debugger = init_machine_debugger(); + let _resume = ResumeVm; + machine_debugger.pause().unwrap(); + assert_eq!(vm_info("VMState"), "paused"); + + let md_count = machine_debugger.get_vcpu_count().unwrap(); + let vmi_count = vboxvmi.get_vcpu_count().unwrap(); + assert_eq!(md_count, vmi_count, "vCPU counts differ"); + assert!( + md_count >= 2, + "cross-method coverage requires VBOX_TEST_CPUS=2 or more" + ); + for vcpu in 0..md_count { + let md = x86_registers(machine_debugger.as_ref(), vcpu); + let vmi = x86_registers(vboxvmi.as_ref(), vcpu); + assert_eq!(md, vmi, "complete registers differ on vCPU {}", vcpu); + } + + let ranges = ram_ranges(); + let ranges_end = ranges.iter().map(|range| range.end).max().unwrap(); + let md_max = machine_debugger.get_max_physical_addr().unwrap(); + let vmi_max = vboxvmi.get_max_physical_addr().unwrap(); + assert_eq!(md_max, vmi_max, "maximum physical addresses differ"); + assert_eq!(md_max, ranges_end, "driver maximum does not cover PGM RAM"); + + let mut md = vec![0; CHUNK as usize]; + let mut vmi = vec![0; CHUNK as usize]; + let (mut compared, mut compared_above_4g) = (0u64, 0u64); + for range in &ranges { + let mut addr = range.start; + while addr < range.end { + let len = CHUNK.min(range.end - addr) as usize; + read_exact( + machine_debugger.as_ref(), + "MachineDebugger", + addr, + &mut md[..len], + ); + read_exact(vboxvmi.as_ref(), "VBoxVMI", addr, &mut vmi[..len]); + if let Some(offset) = md[..len] + .iter() + .zip(&vmi[..len]) + .position(|(md, vmi)| md != vmi) + { + panic!( + "methods differ at guest-physical {:#x}: {:#04x} != {:#04x}", + addr + offset as u64, + md[offset], + vmi[offset] + ); + } + compared += len as u64; + compared_above_4g += (addr + len as u64).saturating_sub(addr.max(FOUR_GIB)); + addr += len as u64; + } + } + let ram_bytes: u64 = ranges.iter().map(|range| range.end - range.start).sum(); + let ram_above_4g: u64 = ranges + .iter() + .map(|range| range.end.saturating_sub(range.start.max(FOUR_GIB))) + .sum(); + assert_eq!(compared, ram_bytes); + assert_eq!(compared_above_4g, ram_above_4g); + if vm_info("memory").parse::().unwrap() > 4096 { + assert!( + md_max > FOUR_GIB, + "maximum physical address is not above 4 GiB" + ); + assert!( + compared_above_4g > 0, + "no RAM bytes above 4 GiB were compared" + ); + } + println!( + "cross-method compared {} RAM bytes ({} above 4 GiB), max physical address {:#x}, {} vCPUs", + compared, compared_above_4g, md_max, md_count + ); + print!("PGM RAM ranges"); + for range in &ranges { + print!(" {:#x}..{:#x}", range.start, range.end); + } + println!(); + + let page_addrs = zero_pages_live(machine_debugger.as_ref(), vboxvmi.as_ref(), &ranges); + if ram_above_4g > 0 { + assert!( + page_addrs.iter().any(|&addr| addr >= FOUR_GIB), + "no write page above 4 GiB" + ); + } + let pages: Vec<(u64, Vec)> = page_addrs + .iter() + .map(|&addr| { + ( + addr, + read_page(machine_debugger.as_ref(), "MachineDebugger", addr), + ) + }) + .collect(); + for (addr, original) in &pages { + assert_page(vboxvmi.as_ref(), "VBoxVMI", *addr, original); + } + let vcpu = md_count - 1; + let registers = x86_registers(machine_debugger.as_ref(), vcpu); + assert_registers(vboxvmi.as_ref(), "VBoxVMI", vcpu, ®isters); + let mut restore = CrossMethodRestore { + machine_debugger: machine_debugger.as_ref(), + vboxvmi: vboxvmi.as_ref(), + pages, + vcpu, + registers, + armed: true, + }; + + for (addr, original) in &restore.pages { + let changed = pattern(*addr, 0x004d_442d_3e56_4d49); + machine_debugger.write_physical(*addr, &changed).unwrap(); + assert_page(vboxvmi.as_ref(), "VBoxVMI", *addr, &changed); + vboxvmi.write_physical(*addr, original).unwrap(); + assert_page( + machine_debugger.as_ref(), + "MachineDebugger", + *addr, + original, + ); + assert_page(vboxvmi.as_ref(), "VBoxVMI", *addr, original); + + let changed = pattern(*addr, 0x0056_4d49_2d3e_4d44); + vboxvmi.write_physical(*addr, &changed).unwrap(); + assert_page( + machine_debugger.as_ref(), + "MachineDebugger", + *addr, + &changed, + ); + } + + let mut changed = restore.registers.clone(); + changed.sysenter_esp ^= 8; + changed.sysenter_eip ^= 8; + machine_debugger + .write_registers(vcpu, Registers::X86(changed.clone())) + .unwrap(); + assert_registers(vboxvmi.as_ref(), "VBoxVMI", vcpu, &changed); + vboxvmi + .write_registers(vcpu, Registers::X86(restore.registers.clone())) + .unwrap(); + assert_registers( + machine_debugger.as_ref(), + "MachineDebugger", + vcpu, + &restore.registers, + ); + assert_registers(vboxvmi.as_ref(), "VBoxVMI", vcpu, &restore.registers); + + changed = restore.registers.clone(); + changed.sysenter_esp ^= 16; + changed.sysenter_eip ^= 16; + vboxvmi + .write_registers(vcpu, Registers::X86(changed.clone())) + .unwrap(); + assert_registers(machine_debugger.as_ref(), "MachineDebugger", vcpu, &changed); + + // Final restoration is deliberately performed through both methods and verified through + // both, so either method can recover the state left by the other. + for (addr, original) in &restore.pages { + vboxvmi.write_physical(*addr, original).unwrap(); + assert_page( + machine_debugger.as_ref(), + "MachineDebugger", + *addr, + original, + ); + assert_page(vboxvmi.as_ref(), "VBoxVMI", *addr, original); + machine_debugger.write_physical(*addr, original).unwrap(); + assert_page( + machine_debugger.as_ref(), + "MachineDebugger", + *addr, + original, + ); + assert_page(vboxvmi.as_ref(), "VBoxVMI", *addr, original); + } + vboxvmi + .write_registers(vcpu, Registers::X86(restore.registers.clone())) + .unwrap(); + assert_registers( + machine_debugger.as_ref(), + "MachineDebugger", + vcpu, + &restore.registers, + ); + assert_registers(vboxvmi.as_ref(), "VBoxVMI", vcpu, &restore.registers); + machine_debugger + .write_registers(vcpu, Registers::X86(restore.registers.clone())) + .unwrap(); + assert_registers( + machine_debugger.as_ref(), + "MachineDebugger", + vcpu, + &restore.registers, + ); + assert_registers(vboxvmi.as_ref(), "VBoxVMI", vcpu, &restore.registers); + restore.armed = false; + let pages: Vec = page_addrs + .iter() + .map(|addr| format!("{:#x}", addr)) + .collect(); + println!( + "cross-method writes restored on vCPU {} and RAM pages {}", + vcpu, + pages.join(", ") + ); + drop(restore); + + machine_debugger.resume().unwrap(); + assert_eq!(vm_info("VMState"), "running"); + } + + inventory::submit!(IntegrationTest { + name: "virtualbox_cross_method_consistency", + test_fn: consistency + }); +} From f46fd74dc285a9142033ce570b05f3cc017ae057 Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Mon, 12 Oct 2026 10:59:26 +0000 Subject: [PATCH 2/2] test: require explicit cross-method filter --- doc/src/developer/tests.md | 16 +++--- tests/common/filter.rs | 5 ++ tests/common/mod.rs | 1 + tests/integration.rs | 6 +-- tests/integration_filter.rs | 12 +++++ tests/tests/virtualbox.rs | 104 ++++++++++++++++++++++-------------- 6 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 tests/common/filter.rs create mode 100644 tests/integration_filter.rs diff --git a/doc/src/developer/tests.md b/doc/src/developer/tests.md index 4b168f3d..50831155 100644 --- a/doc/src/developer/tests.md +++ b/doc/src/developer/tests.md @@ -92,7 +92,7 @@ and write the last one. ### MachineDebugger/VBoxVMI cross-method consistency -The `virtualbox_cross_method_consistency` inventory test opens MachineDebugger and VBoxVMI +The `cross_method_consistency` inventory test opens MachineDebugger and VBoxVMI concurrently against the same VM process, pauses it through MachineDebugger, and compares every register on every vCPU and every byte in the live PGM RAM ranges directly between the two methods. It checks both drivers' maximum physical address against the end of those ranges, reports the exact @@ -114,16 +114,18 @@ git -C /path/to/virtualbox worktree add /path/to/virtualbox-vmi fork/vmi/cross-m ~~~ Compile the integration binary with both access methods. Run it on the host with -`VBOX_APP_HOME` pointing at that same combined build: this is required both for the VM's VBoxVMI -server and for MachineDebugger's dynamically loaded `VBoxC.so`. `VBOX_TEST_METHOD=vboxvmi` makes -the context enable the server and supplies its one client to the cross-method test. The test -requires two vCPUs. Run both the 512 MiB case and the 5120 MiB case, which must report nonzero bytes -and a selected write page above 4 GiB: +`VBOX_APP_HOME` pointing at that same combined build: the VM's VBoxVMI server and +MachineDebugger's dynamically loaded `VBoxC.so` must come from one build. +`VBOX_TEST_METHOD=vboxvmi` enables the server and supplies its one client to the test. +`VBOX_TEST_CROSS_METHOD=1` opts into this destructive live proof; without it an unfiltered run +prints an explicit skip, and ordinary `virtualbox_` filters do not select it. The test requires +the exact `MICROVMI_TEST_FILTER=cross_method_consistency` and two vCPUs. Run both the 512 MiB case +and the 5120 MiB case, which must report nonzero bytes and a selected write page above 4 GiB: ~~~ cargo test --features 'virtualbox-api virtualbox-vmi' --test integration --no-run BIN=target/debug/deps/integration- -COMMON='VBOX_APP_HOME=/path/to/virtualbox-vmi/out/linux.amd64/release/bin VBOX_USER_HOME=/path/to/vbox-home VBOX_TEST_METHOD=vboxvmi VBOX_TEST_CPUS=2 MICROVMI_TEST_FILTER=cross_method_consistency MICROVMI_TEST_TIMEOUT=180 VBOX_TEST_CORE_DIR=/path/to/disk-backed/core-dir' +COMMON='VBOX_APP_HOME=/path/to/virtualbox-vmi/out/linux.amd64/release/bin VBOX_USER_HOME=/path/to/vbox-home VBOX_TEST_METHOD=vboxvmi VBOX_TEST_CROSS_METHOD=1 VBOX_TEST_CPUS=2 MICROVMI_TEST_FILTER=cross_method_consistency MICROVMI_TEST_TIMEOUT=180 VBOX_TEST_CORE_DIR=/path/to/disk-backed/core-dir' flock -o /path/to/vbox-dev/.vm.lock env $COMMON VBOX_TEST_MEMORY_MB=512 $BIN flock -o /path/to/vbox-dev/.vm.lock env $COMMON VBOX_TEST_MEMORY_MB=5120 $BIN ~~~ diff --git a/tests/common/filter.rs b/tests/common/filter.rs new file mode 100644 index 00000000..b2b73530 --- /dev/null +++ b/tests/common/filter.rs @@ -0,0 +1,5 @@ +pub const CROSS_METHOD_TEST: &str = "cross_method_consistency"; + +pub fn is_selected(name: &str, filter: Option<&str>) -> bool { + filter.is_none_or(|filter| name.contains(filter)) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 01c39d30..ab6f4219 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,5 +1,6 @@ pub mod config; pub mod context; +pub mod filter; #[cfg(feature = "kvm")] mod kvm; #[cfg(all( diff --git a/tests/integration.rs b/tests/integration.rs index 7af5bb46..82c9ad7c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -8,6 +8,7 @@ use std::time::Duration; use colored::*; use common::context::init_context; +use common::filter::is_selected; use tests::IntegrationTest; mod common; @@ -26,10 +27,7 @@ fn main() { let (mut ran, mut failed) = (0, 0); // for each test for test in inventory::iter:: { - if filter - .as_ref() - .is_some_and(|f| !test.name.contains(f.as_str())) - { + if !is_selected(test.name, filter.as_deref()) { continue; } ran += 1; diff --git a/tests/integration_filter.rs b/tests/integration_filter.rs new file mode 100644 index 00000000..4a915960 --- /dev/null +++ b/tests/integration_filter.rs @@ -0,0 +1,12 @@ +#![cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] + +#[path = "common/filter.rs"] +mod filter; + +use filter::{is_selected, CROSS_METHOD_TEST}; + +#[test] +fn cross_method_requires_its_exact_filter() { + assert!(!is_selected(CROSS_METHOD_TEST, Some("virtualbox_"))); + assert!(is_selected(CROSS_METHOD_TEST, Some(CROSS_METHOD_TEST))); +} diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index e92f4e41..a5910af3 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -1053,6 +1053,7 @@ inventory::submit!(IntegrationTest { #[cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] mod cross_method { use super::*; + use crate::common::filter::CROSS_METHOD_TEST; use microvmi::api::params::{CommonInitParams, DriverInitParams, VBoxInitParams}; use microvmi::api::DriverType; @@ -1112,44 +1113,6 @@ mod cross_method { ); } - /// The same safe page choices as the core-based write test, found directly through both live - /// methods: the first all-zero page below low reserved RAM, mid-RAM, low RAM and top RAM. - fn zero_pages_live( - machine_debugger: &dyn Introspectable, - vboxvmi: &dyn Introspectable, - ranges: &[Range], - ) -> Vec { - let top = ranges.iter().map(|range| range.end).max().unwrap(); - let top_below_4g = ranges - .iter() - .map(|range| range.end) - .filter(|&end| end <= FOUR_GIB) - .max() - .unwrap(); - let mut pages: Vec = [0x10000, top / 2, top_below_4g, top] - .iter() - .map(|&target| { - let range = ranges - .iter() - .find(|range| range.start < target && target <= range.end) - .unwrap_or_else(|| panic!("{:#x} is not in a RAM range", target)); - (1..=1024) - .filter_map(|i| (target & !(PAGE - 1)).checked_sub(i * PAGE)) - .take_while(|&page| page >= range.start.max(PAGE)) - .find(|&page| { - let md = read_page(machine_debugger, "MachineDebugger", page); - let vmi = read_page(vboxvmi, "VBoxVMI", page); - assert_eq!(md, vmi, "methods differ at candidate page {:#x}", page); - md.iter().all(|&byte| byte == 0) - }) - .unwrap_or_else(|| panic!("no zero page below {:#x}", target)) - }) - .collect(); - pages.sort_unstable(); - pages.dedup(); - pages - } - fn pattern(addr: u64, salt: u64) -> Vec { let mut state = (addr ^ salt) | 1; (0..PAGE) @@ -1217,6 +1180,24 @@ mod cross_method { /// directly between the methods. Then write safe RAM pages and SYSENTER registers in both /// directions, with independent readback and exact restoration through both methods. fn consistency(vboxvmi: Box) { + let filter = env::var("MICROVMI_TEST_FILTER").ok(); + if !matches!(env::var("VBOX_TEST_CROSS_METHOD").as_deref(), Ok("1")) { + if filter.is_none() { + println!( + "SKIPPED: set VBOX_TEST_CROSS_METHOD=1 and \ + MICROVMI_TEST_FILTER={}", + CROSS_METHOD_TEST + ); + return; + } + panic!("cross-method test requires VBOX_TEST_CROSS_METHOD=1"); + } + assert_eq!( + filter.as_deref(), + Some(CROSS_METHOD_TEST), + "cross-method test requires the exact MICROVMI_TEST_FILTER={}", + CROSS_METHOD_TEST + ); assert!( matches!(env::var("VBOX_TEST_METHOD").as_deref(), Ok("vboxvmi")), "cross-method test requires VBOX_TEST_METHOD=vboxvmi" @@ -1246,6 +1227,28 @@ mod cross_method { assert_eq!(md_max, vmi_max, "maximum physical addresses differ"); assert_eq!(md_max, ranges_end, "driver maximum does not cover PGM RAM"); + // The same safe locations as the core-based write test. Record the nearest all-zero page + // below each target while the complete direct comparison already has every RAM page. + let top = ranges_end; + let top_below_4g = ranges + .iter() + .map(|range| range.end) + .filter(|&end| end <= FOUR_GIB) + .max() + .unwrap(); + let write_targets = [0x10000, top / 2, top_below_4g, top]; + let target_ranges: Vec<(u64, u64)> = write_targets + .iter() + .map(|&target| { + let range = ranges + .iter() + .find(|range| range.start < target && target <= range.end) + .unwrap_or_else(|| panic!("{:#x} is not in a RAM range", target)); + (target & !(PAGE - 1), range.start.max(PAGE)) + }) + .collect(); + let mut write_pages = vec![None; write_targets.len()]; + let mut md = vec![0; CHUNK as usize]; let mut vmi = vec![0; CHUNK as usize]; let (mut compared, mut compared_above_4g) = (0u64, 0u64); @@ -1272,6 +1275,19 @@ mod cross_method { vmi[offset] ); } + let (pages, remainder) = md[..len].as_chunks::<{ PAGE as usize }>(); + debug_assert!(remainder.is_empty()); + for (offset, page) in pages.iter().enumerate() { + if page.iter().any(|&byte| byte != 0) { + continue; + } + let page = addr + offset as u64 * PAGE; + for ((target, first), selected) in target_ranges.iter().zip(&mut write_pages) { + if *first <= page && page < *target { + *selected = Some(page); + } + } + } compared += len as u64; compared_above_4g += (addr + len as u64).saturating_sub(addr.max(FOUR_GIB)); addr += len as u64; @@ -1304,7 +1320,15 @@ mod cross_method { } println!(); - let page_addrs = zero_pages_live(machine_debugger.as_ref(), vboxvmi.as_ref(), &ranges); + let mut page_addrs: Vec = write_pages + .into_iter() + .zip(write_targets) + .map(|(page, target)| { + page.unwrap_or_else(|| panic!("no zero page below {:#x}", target)) + }) + .collect(); + page_addrs.sort_unstable(); + page_addrs.dedup(); if ram_above_4g > 0 { assert!( page_addrs.iter().any(|&addr| addr >= FOUR_GIB), @@ -1441,7 +1465,7 @@ mod cross_method { } inventory::submit!(IntegrationTest { - name: "virtualbox_cross_method_consistency", + name: CROSS_METHOD_TEST, test_fn: consistency }); }