From da9160d572fe22aa2c0eba06c329146000854d8b Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Mon, 12 Oct 2026 09:43:48 +0000 Subject: [PATCH 1/2] virtualbox: write guest physical memory over VBoxVMI --- Cargo.lock | 2 +- Cargo.toml | 2 +- doc/src/developer/tests.md | 8 +++---- doc/src/reference/drivers/virtualbox.md | 7 +++--- src/driver/virtualbox_vmi.rs | 30 +++++++++++++++++-------- tests/tests/virtualbox.rs | 6 +++++ 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d947336..ce5f4218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "vboxvmi" version = "0.1.0" -source = "git+https://github.com/Wenzel/rs-vboxvmi?rev=5452496d5f26dc519e7aea39dc3c58a3658557dc#5452496d5f26dc519e7aea39dc3c58a3658557dc" +source = "git+https://github.com/Wenzel/rs-vboxvmi?rev=9a349ca535b612232a0d0b26031fedadd3d0ecb0#9a349ca535b612232a0d0b26031fedadd3d0ecb0" dependencies = [ "libc", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 8b3f83dc..1f465fd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ xenvmevent-sys = { version = "=0.2.0", optional = true } kvmi = { version = "=0.5.0", optional = true } fdp = { version = "=0.3.0", optional = true } vboxapi = { git = "https://github.com/Wenzel/rs-vboxapi", rev = "5ba8d86d1cc58a5935208588580782a31fb2d27c", optional = true } -vboxvmi = { git = "https://github.com/Wenzel/rs-vboxvmi", rev = "5452496d5f26dc519e7aea39dc3c58a3658557dc", optional = true } +vboxvmi = { git = "https://github.com/Wenzel/rs-vboxvmi", rev = "9a349ca535b612232a0d0b26031fedadd3d0ecb0", optional = true } winapi = { version = "0.3", features = [ "tlhelp32", "winnt", diff --git a/doc/src/developer/tests.md b/doc/src/developer/tests.md index bc5c347e..a55b2e44 100644 --- a/doc/src/developer/tests.md +++ b/doc/src/developer/tests.md @@ -75,13 +75,13 @@ Run the generic tests the method supports, or the VirtualBox ones: MICROVMI_TEST_FILTER=virtualbox_ cargo test --features virtualbox-api --test integration MICROVMI_TEST_FILTER=virtualbox_ VBOX_TEST_MEMORY_MB=5120 cargo test --features virtualbox-api --test integration MICROVMI_TEST_FILTER=vcpu VBOX_TEST_CPUS=2 cargo test --features virtualbox-api --test integration -for f in init vcpu_count pause double_resume virtualbox_read_ registers_match write_registers; do MICROVMI_TEST_FILTER=$f VBOX_TEST_METHOD=vboxvmi cargo test --features virtualbox-vmi --test integration; done +for f in init vcpu_count pause double_resume virtualbox_read_ virtualbox_write_physical_ registers_match write_registers; do MICROVMI_TEST_FILTER=$f VBOX_TEST_METHOD=vboxvmi cargo test --features virtualbox-vmi --test integration; done for f in vcpu pause registers_match write_registers; do MICROVMI_TEST_FILTER=$f VBOX_TEST_CPUS=2 VBOX_TEST_METHOD=vboxvmi cargo test --features virtualbox-vmi --test integration; done ~~~ -The VBoxVMI method supports every test but the `virtualbox_write_physical_*` ones, which are -MachineDebugger-only until it writes memory ([#285](https://github.com/Wenzel/libmicrovmi/issues/285)). -The VBoxVMI filters above run every test it supports, each once: `pause` covers `pause`, +The VBoxVMI filters above run every test, each once; `virtualbox_write_physical_` checks random +patterns against a fresh core dump, after resume/re-pause and after restoring the original pages. +The other filters cover `pause`, `double_pause`, `pause_resume`, `multiple_pause_resume`, `virtualbox_pause_*` and `virtualbox_registers_unchanged_while_paused`; `vcpu_count` covers `get_vcpu_count` and `virtualbox_vcpu_count_matches_vmm`. With `VBOX_TEST_CPUS=2`, the register tests read every vCPU diff --git a/doc/src/reference/drivers/virtualbox.md b/doc/src/reference/drivers/virtualbox.md index a8bb0e08..487c90f3 100644 --- a/doc/src/reference/drivers/virtualbox.md +++ b/doc/src/reference/drivers/virtualbox.md @@ -17,9 +17,10 @@ write with MachineDebugger or VBoxVMI fails (before writing anything) unless the Speaks VBoxVMI, a protocol served from inside the VM process of a patched VirtualBox over a per-VM Unix socket. It is the fast path, and the way towards VMI events. -Supported: reading guest physical memory, reading and writing the complete x86 register set, max physical address (end of the highest guest RAM range, so RAM above 4 GiB is included), pause/resume, vCPU count. -Reads are served inside the VM process without waiting for a vCPU, in batches of up to 15 pages per request. +Supported: reading and writing guest physical memory, reading and writing the complete x86 register set, max physical address (end of the highest guest RAM range, so RAM above 4 GiB is included), pause/resume, vCPU count. +Physical reads and writes are served inside the VM process without waiting for a vCPU, batching pages into requests of at most 64 KiB. A read that hits an unreadable page (MMIO, including the local APIC page, or no memory) fails, with `bytes_read` set to the bytes read before that page; devices are never accessed. +A physical write to a non-RAM page fails; other pages in the same batch may already have been written, so writes are not atomic. MMIO devices are never accessed. Pause is VirtualBox's own pause (the VM shows as Paused in the GUI and `VBoxManage`). It is synchronous, all vCPUs are stopped when it returns, and idempotent: pausing a paused VM succeeds, and one resume resumes it. Pauses are not counted, also across clients: resume resumes the VM even if the GUI, `VBoxManage` or another tool paused it too. Pause and resume fail while VirtualBox controls the VM's run state: powering on or off, saving or restoring its state, live snapshot, teleport, or a configuration change (medium or network attachment). Resume also fails while the host's power management has the VM paused, and after a failed teleport of a paused VM until it is resumed through VirtualBox. @@ -29,8 +30,6 @@ The MSRs of `X86Registers` (EFER, STAR, LSTAR, SYSENTER_CS/ESP/EIP, APIC base) a A write is all or nothing: a value the CPU itself would not load (RFLAGS with bit 1 clear or a reserved bit set, reserved bits in CR0, CR3, CR4 or EFER, an inconsistent long mode state, an MSR value `WRMSR` refuses) fails the whole write, and nothing is written. Registers keeping their value are not touched, so writing back registers read earlier always succeeds and has no side effects. Segment selectors, bases and limits are written as raw state: no descriptor table load, the segment attributes are kept, and keeping them consistent is up to the caller. Writes fail while the VM is neither running nor paused (powering on or off, saving or restoring its state, live snapshot, teleport). -Not implemented yet: writing guest physical memory ([#285](https://github.com/Wenzel/libmicrovmi/issues/285)): it returns a "not implemented yet" error. - ### Requirements - Platform: Linux diff --git a/src/driver/virtualbox_vmi.rs b/src/driver/virtualbox_vmi.rs index 841c7579..681fd309 100644 --- a/src/driver/virtualbox_vmi.rs +++ b/src/driver/virtualbox_vmi.rs @@ -11,14 +11,10 @@ use crate::driver::EferMismatch; #[derive(thiserror::Error, Debug)] pub enum VBoxVmiDriverError { - #[error("VirtualBox driver, VBoxVMI method: {0} is not implemented yet")] - NotImplemented(&'static str), #[error("VirtualBox driver, VBoxVMI method: guest physical page {addr:#x} is unreadable (VirtualBox status {rc})")] Unreadable { addr: u64, rc: i32 }, -} - -fn not_implemented(what: &'static str) -> Result> { - Err(VBoxVmiDriverError::NotImplemented(what).into()) + #[error("VirtualBox driver, VBoxVMI method: guest physical page {addr:#x} is unwritable (VirtualBox status {rc})")] + Unwritable { addr: u64, rc: i32 }, } fn segment_reg(s: &Segment) -> SegmentReg { @@ -78,7 +74,6 @@ impl VBoxVmi { } } -// shortcut: physical memory write errors until the protocol has the command (#285). impl Introspectable for VBoxVmi { fn get_vcpu_count(&self) -> Result> { Ok(self.vcpu_count) @@ -115,8 +110,25 @@ impl Introspectable for VBoxVmi { Ok(()) } - fn write_physical(&self, _paddr: u64, _buf: &[u8]) -> Result<(), Box> { - not_implemented("physical memory write") + /// A failed page leaves the successful prefix written; later pages may also have been written. + fn write_physical(&self, paddr: u64, buf: &[u8]) -> Result<(), Box> { + let mut status = Vec::new(); + let result = self + .client + .borrow_mut() + .write_physical(&[(paddr, buf)], &mut status); + let mut offset = 0usize; + for rc in status { + let addr = paddr + offset as u64; + if rc < 0 { + return Err(VBoxVmiDriverError::Unwritable { addr, rc }.into()); + } + offset += + (buf.len() - offset).min((vboxvmi::PAGE_SIZE - addr % vboxvmi::PAGE_SIZE) as usize); + } + result?; + debug_assert_eq!(offset, buf.len()); + Ok(()) } fn get_max_physical_addr(&self) -> Result> { diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index aae2f17b..098de886 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -389,6 +389,12 @@ fn write_physical_matches_core(drv: Box) { let ranges = ram_ranges(); let before = Core::dump(); let page_addrs = zero_pages(&before, &ranges); + if ranges.iter().any(|r| r.end > FOUR_GIB) { + assert!( + page_addrs.iter().any(|&page| page >= FOUR_GIB), + "write test did not select RAM above 4 GiB" + ); + } let pages: Vec<(u64, Vec)> = page_addrs .iter() .map(|&page| (page, before.ram(page, PAGE))) From 3cc62d310796c54a3a1dbb6eae192a1e236c4cd0 Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Mon, 12 Oct 2026 09:49:05 +0000 Subject: [PATCH 2/2] virtualbox: resume guest if write-test setup fails --- tests/tests/virtualbox.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index 098de886..cf35f69f 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -318,7 +318,17 @@ inventory::submit!(IntegrationTest { test_fn: pause_idempotent_and_visible }); -/// Restores the original page contents and the running VM state, including during unwinding. +/// Resumes the VM even if selecting pages or dumping the first core fails. +struct ResumeVm; + +impl Drop for ResumeVm { + fn drop(&mut self) { + let vm = vm_name(); + let _ = try_vboxmanage(&["controlvm", &vm, "resume"]); + } +} + +/// Restores original page contents before the VM is resumed, including during unwinding. struct RestorePages<'a> { drv: &'a dyn Introspectable, pages: &'a [(u64, Vec)], @@ -343,8 +353,6 @@ impl Drop for RestorePages<'_> { let _ = self.drv.write_physical(*page, original); } } - let vm = vm_name(); - let _ = try_vboxmanage(&["controlvm", &vm, "resume"]); } } @@ -385,6 +393,7 @@ fn zero_pages(core: &Core, ranges: &[Range]) -> Vec { /// on success and during unwinding. `VBOX_TEST_SEED` replays a pattern. fn write_physical_matches_core(drv: Box) { let vm = vm_name(); + let _resume = ResumeVm; vboxmanage(&["controlvm", &vm, "pause"]); let ranges = ram_ranges(); let before = Core::dump();