From a075c1bcd88bc1a5bab90050b3ad9898e6726c29 Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Mon, 12 Oct 2026 08:26:11 +0000 Subject: [PATCH 1/2] virtualbox: VBoxVMI method reads and writes registers read_registers and write_registers go through the VBoxVMI REGS_READ and REGS_WRITE commands: the full X86Registers set of one vCPU, MSRs included, in one round trip, served by the vCPU's EMT between two of its instructions (a consistent snapshot, an atomic write, also on a running VM). A write is all or nothing: a value the CPU would not load (e.g. RFLAGS bit 1 clear) fails it without writing anything. efer and msr_efer are both EFER and must be equal in a write. rs-vboxvmi is bumped to 5452496 for the new commands. The register tests run under both methods: the invalid RFLAGS check goes through the driver instead of VBoxManage setregisters (which needs the MachineDebugger patch), registers_unchanged_while_paused checks that an unknown vCPU is an error, and the new virtualbox_write_registers_survive_resume checks that written registers are still there after the guest ran. The driver and test docs list registers as supported by VBoxVMI, with filters running every VBoxVMI test once, for 1 and 2 vCPUs. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- doc/src/developer/tests.md | 17 +-- doc/src/reference/drivers/virtualbox.md | 8 +- src/driver/virtualbox_vmi.rs | 151 ++++++++++++++++++++++-- tests/tests/virtualbox.rs | 64 +++++++--- 6 files changed, 206 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0072dc98..3d947336 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=2b71bb814ac7dc8948e1ca90bec88755f362c69c#2b71bb814ac7dc8948e1ca90bec88755f362c69c" +source = "git+https://github.com/Wenzel/rs-vboxvmi?rev=5452496d5f26dc519e7aea39dc3c58a3658557dc#5452496d5f26dc519e7aea39dc3c58a3658557dc" dependencies = [ "libc", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index a91d45d8..8b3f83dc 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 = "2b71bb814ac7dc8948e1ca90bec88755f362c69c", optional = true } +vboxvmi = { git = "https://github.com/Wenzel/rs-vboxvmi", rev = "5452496d5f26dc519e7aea39dc3c58a3658557dc", optional = true } winapi = { version = "0.3", features = [ "tlhelp32", "winnt", diff --git a/doc/src/developer/tests.md b/doc/src/developer/tests.md index 10a84df0..bc5c347e 100644 --- a/doc/src/developer/tests.md +++ b/doc/src/developer/tests.md @@ -75,16 +75,17 @@ 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 double_pause pause_ double_resume virtualbox_read_; do MICROVMI_TEST_FILTER=$f VBOX_TEST_METHOD=vboxvmi cargo test --features virtualbox-vmi --test integration; done -MICROVMI_TEST_FILTER=vcpu VBOX_TEST_CPUS=2 VBOX_TEST_METHOD=vboxvmi cargo test --features virtualbox-vmi --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 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 reads memory, pauses and resumes, and counts vCPUs so far, but has no registers or -writes ([#284](https://github.com/Wenzel/libmicrovmi/issues/284), [#285](https://github.com/Wenzel/libmicrovmi/issues/285)): -the `virtualbox_registers_*`, `virtualbox_write_registers_*` and `virtualbox_write_physical_*` tests are -MachineDebugger-only. The filters above run every test VBoxVMI supports, each once: `pause_` covers -`pause_resume`, `multiple_pause_resume` and `virtualbox_pause_*`. A plain `pause` filter would also run -`virtualbox_registers_unchanged_while_paused`; the `pause` test itself is covered by `double_pause`. +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`, +`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 +and write the last one. If cargo runs in a container without VirtualBox, build the test binary there and run it on the host: diff --git a/doc/src/reference/drivers/virtualbox.md b/doc/src/reference/drivers/virtualbox.md index 7df97517..2d74b319 100644 --- a/doc/src/reference/drivers/virtualbox.md +++ b/doc/src/reference/drivers/virtualbox.md @@ -14,15 +14,19 @@ MachineDebugger and FDP name the VM with `common.vm_name`; VBoxVMI names it by i 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, max physical address (end of the highest guest RAM range, so RAM above 4 GiB is included), pause/resume, vCPU count. +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. 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. 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. A VM paused through VBoxVMI stays paused if the tool exits or disconnects without resuming it ([#287](https://github.com/Wenzel/libmicrovmi/issues/287)). +A register read or write is one request, served by the vCPU itself between two of its instructions: a read is a consistent snapshot of that vCPU and a write changes all its registers at once, also while the VM runs (pause first to read or write several vCPUs at the same point). +The MSRs of `X86Registers` (EFER, STAR, LSTAR, SYSENTER_CS/ESP/EIP, APIC base) are included; an MSR the guest CPU doesn't have reads as 0. `efer` and `msr_efer` are both EFER: a write needs them equal. +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: registers, writes ([#284](https://github.com/Wenzel/libmicrovmi/issues/284)–[#285](https://github.com/Wenzel/libmicrovmi/issues/285)): they return a "not implemented yet" error. +Not implemented yet: writing guest physical memory ([#285](https://github.com/Wenzel/libmicrovmi/issues/285)): it returns a "not implemented yet" error. ### Requirements diff --git a/src/driver/virtualbox_vmi.rs b/src/driver/virtualbox_vmi.rs index af67d2c3..dff578f8 100644 --- a/src/driver/virtualbox_vmi.rs +++ b/src/driver/virtualbox_vmi.rs @@ -3,9 +3,9 @@ use std::cell::RefCell; use std::convert::TryFrom; use std::error::Error; -use vboxvmi::Client; +use vboxvmi::{Client, Segment, TableRegister}; -use crate::api::registers::Registers; +use crate::api::registers::{Registers, SegmentReg, SystemTableReg, X86Registers}; use crate::api::{DriverType, Introspectable}; #[derive(thiserror::Error, Debug)] @@ -14,12 +14,44 @@ pub enum VBoxVmiDriverError { NotImplemented(&'static str), #[error("VirtualBox driver, VBoxVMI method: guest physical page {addr:#x} is unreadable (VirtualBox status {rc})")] Unreadable { addr: u64, rc: i32 }, + #[error("VirtualBox driver, VBoxVMI method: efer {efer:#x} and msr_efer {msr_efer:#x} are both EFER and must be equal")] + EferMismatch { efer: u64, msr_efer: u64 }, } fn not_implemented(what: &'static str) -> Result> { Err(VBoxVmiDriverError::NotImplemented(what).into()) } +fn segment_reg(s: &Segment) -> SegmentReg { + SegmentReg { + base: s.base, + limit: s.limit, + selector: s.selector, + } +} + +fn segment(s: &SegmentReg) -> Segment { + Segment { + base: s.base, + limit: s.limit, + selector: s.selector, + } +} + +fn system_table_reg(t: &TableRegister) -> SystemTableReg { + SystemTableReg { + base: t.base, + limit: t.limit, + } +} + +fn table_register(t: &SystemTableReg) -> TableRegister { + TableRegister { + base: t.base, + limit: t.limit, + } +} + pub struct VBoxVmi { /// `RefCell`: requests need `&mut Client`, reads only `&self`. client: RefCell, @@ -47,7 +79,7 @@ impl VBoxVmi { } } -// shortcut: VM state access errors until the protocol has the commands (#284-#285). +// 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) @@ -92,12 +124,117 @@ impl Introspectable for VBoxVmi { Ok(self.max_addr) } - fn read_registers(&self, _vcpu: u16) -> Result> { - not_implemented("register read") + /// One request, served between two instructions of the vCPU: a consistent snapshot of it, + /// also while the VM runs. The MSRs `X86Registers` has (EFER, STAR, LSTAR, SYSENTER_*, APIC + /// base) are all read, 0 if the guest CPU doesn't have one; `efer` and `msr_efer` are both + /// EFER. + fn read_registers(&self, vcpu: u16) -> Result> { + let r = self.client.borrow_mut().read_registers(vcpu.into())?; + Ok(Registers::X86(X86Registers { + rax: r.rax, + rbx: r.rbx, + rcx: r.rcx, + rdx: r.rdx, + rsi: r.rsi, + rdi: r.rdi, + rsp: r.rsp, + rbp: r.rbp, + r8: r.r8, + r9: r.r9, + r10: r.r10, + r11: r.r11, + r12: r.r12, + r13: r.r13, + r14: r.r14, + r15: r.r15, + rip: r.rip, + rflags: r.rflags, + cr0: r.cr0, + cr2: r.cr2, + cr3: r.cr3, + cr4: r.cr4, + sysenter_cs: r.sysenter_cs, + sysenter_esp: r.sysenter_esp, + sysenter_eip: r.sysenter_eip, + msr_efer: r.efer, + msr_star: r.star, + msr_lstar: r.lstar, + efer: r.efer, + apic_base: r.apic_base, + cs: segment_reg(&r.cs), + ds: segment_reg(&r.ds), + es: segment_reg(&r.es), + fs: segment_reg(&r.fs), + gs: segment_reg(&r.gs), + ss: segment_reg(&r.ss), + tr: segment_reg(&r.tr), + ldt: segment_reg(&r.ldtr), + idt: system_table_reg(&r.idtr), + gdt: system_table_reg(&r.gdtr), + })) } - fn write_registers(&self, _vcpu: u16, _reg: Registers) -> Result<(), Box> { - not_implemented("register write") + /// One request, applied between two instructions of the vCPU, all registers at once, also + /// while the VM runs (pause to write several vCPUs at the same point). All or nothing: a value + /// the CPU would not load (RFLAGS bit 1 clear or reserved bits set, reserved control register + /// or EFER bits, an MSR value `WRMSR` refuses) fails the write and nothing is written. + /// Registers keeping their value are not touched. Segment selectors, bases and limits are + /// written as raw state (no descriptor table load, attributes kept). `efer` and `msr_efer` + /// are both EFER: they must be equal. + fn write_registers(&self, vcpu: u16, reg: Registers) -> Result<(), Box> { + let Registers::X86(r) = reg; + if r.efer != r.msr_efer { + return Err(VBoxVmiDriverError::EferMismatch { + efer: r.efer, + msr_efer: r.msr_efer, + } + .into()); + } + let regs = vboxvmi::X86Registers { + rax: r.rax, + rcx: r.rcx, + rdx: r.rdx, + rbx: r.rbx, + rsp: r.rsp, + rbp: r.rbp, + rsi: r.rsi, + rdi: r.rdi, + r8: r.r8, + r9: r.r9, + r10: r.r10, + r11: r.r11, + r12: r.r12, + r13: r.r13, + r14: r.r14, + r15: r.r15, + rip: r.rip, + rflags: r.rflags, + cr0: r.cr0, + cr2: r.cr2, + cr3: r.cr3, + cr4: r.cr4, + efer: r.efer, + star: r.msr_star, + lstar: r.msr_lstar, + sysenter_cs: r.sysenter_cs, + sysenter_esp: r.sysenter_esp, + sysenter_eip: r.sysenter_eip, + apic_base: r.apic_base, + es: segment(&r.es), + cs: segment(&r.cs), + ss: segment(&r.ss), + ds: segment(&r.ds), + fs: segment(&r.fs), + gs: segment(&r.gs), + ldtr: segment(&r.ldt), + tr: segment(&r.tr), + gdtr: table_register(&r.gdt), + idtr: table_register(&r.idt), + }; + Ok(self + .client + .borrow_mut() + .write_registers(vcpu.into(), ®s)?) } /// Synchronous and idempotent: one `resume` undoes any number of `pause`s. diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index 2fa737da..9b89c8c9 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -552,10 +552,13 @@ impl Drop for RegisterRestore { } /// While paused, two reads of every vCPU's registers are identical, not all zero, and RFLAGS -/// is architecturally valid. The multi-vCPU check needs `VBOX_TEST_CPUS=2`. +/// is architecturally valid; a vCPU past the count is an error. The multi-vCPU check needs +/// `VBOX_TEST_CPUS=2`. fn registers_unchanged_while_paused(mut drv: Box) { drv.pause().unwrap(); - for vcpu in 0..drv.get_vcpu_count().unwrap() { + let count = drv.get_vcpu_count().unwrap(); + assert!(drv.read_registers(count).is_err(), "vCPU {} read", count); + for vcpu in 0..count { let first = x86_registers(drv.as_ref(), vcpu); sleep(Duration::from_millis(500)); let second = x86_registers(drv.as_ref(), vcpu); @@ -691,24 +694,17 @@ fn write_registers_reads_back(drv: Box) { .unwrap_or_else(|| panic!("unexpected cs_attr.dpl output: {:?}", cs_dpl)); assert!(dpl <= 3, "invalid CS DPL {}", dpl); - let rflags_error = try_vboxmanage(&[ - "debugvm", - &vm_name(), - "setregisters", - "--cpu", - &vcpu.to_string(), - "rflags=0", - ]) - .expect_err("an RFLAGS value with bit 1 clear was accepted"); - assert!( - rflags_error.contains("VERR_INVALID_PARAMETER"), - "unexpected invalid-RFLAGS error: {}", - rflags_error - ); + let mut invalid = restore.original.clone(); + invalid.rflags = 0; // bit 1 clear + let rflags_error = restore + .drv + .write_registers(vcpu, Registers::X86(invalid)) + .expect_err("an RFLAGS value with bit 1 clear was accepted"); + print!("invalid RFLAGS refused: {}, ", rflags_error); assert_eq!( - x86_registers(restore.drv.as_ref(), vcpu).rflags, - restore.original.rflags, - "invalid RFLAGS write changed the register" + x86_registers(restore.drv.as_ref(), vcpu), + restore.original, + "invalid RFLAGS write changed the registers" ); expected.r12 ^= 0x5a5a_5a5a_5a5a_5a5a; @@ -762,3 +758,33 @@ inventory::submit!(IntegrationTest { name: "virtualbox_write_registers_reads_back", test_fn: write_registers_reads_back }); + +/// A register write reaches the vCPU itself, not only VirtualBox's copy of its state: SYSENTER_ESP +/// and SYSENTER_EIP (unused by an idle 64-bit Linux guest) written while paused keep their new +/// values after the guest ran, and the guest keeps running. The guard restores them. +fn write_registers_survive_resume(drv: Box) { + let mut restore = RegisterRestore::new(drv); + let vcpu = restore.vcpu; + let mut changed = restore.original.clone(); + changed.sysenter_esp ^= 8; + changed.sysenter_eip ^= 8; + restore + .drv + .write_registers(vcpu, Registers::X86(changed.clone())) + .unwrap(); + restore.drv.resume().unwrap(); + sleep(Duration::from_secs(1)); + assert_eq!(vm_info("VMState"), "running"); + restore.drv.pause().unwrap(); + let after = x86_registers(restore.drv.as_ref(), vcpu); + assert_eq!( + (after.sysenter_esp, after.sysenter_eip), + (changed.sysenter_esp, changed.sysenter_eip) + ); + restore.finish(); +} + +inventory::submit!(IntegrationTest { + name: "virtualbox_write_registers_survive_resume", + test_fn: write_registers_survive_resume +}); From 7db6776a2e00b45d82e4208dc5680b488946f7fe Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Mon, 12 Oct 2026 09:08:55 +0000 Subject: [PATCH 2/2] virtualbox: both methods refuse writing efer and msr_efer apart VirtualBox has one EFER register, which X86Registers::efer and msr_efer both are. A register write giving them different values now fails with the shared EferMismatch error before anything is written, also with the MachineDebugger method, which wrote both to the same DBGF register and let the last one win. Documented on the driver page and the efer field. virtualbox_write_registers_survive_resume waits until a cold-booted guest has set SYSENTER_EIP on the vCPU it writes, and restores only the two MSRs it changed: the other registers moved on while the guest ran. Co-Authored-By: Claude Opus 5.5 --- doc/src/reference/drivers/virtualbox.md | 5 ++++- src/api/registers.rs | 3 ++- src/driver/mod.rs | 26 +++++++++++++++++++++++++ src/driver/virtualbox_api.rs | 8 +++++--- src/driver/virtualbox_vmi.rs | 11 ++--------- tests/tests/virtualbox.rs | 25 +++++++++++++++++++++--- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/doc/src/reference/drivers/virtualbox.md b/doc/src/reference/drivers/virtualbox.md index 2d74b319..a8bb0e08 100644 --- a/doc/src/reference/drivers/virtualbox.md +++ b/doc/src/reference/drivers/virtualbox.md @@ -3,6 +3,9 @@ The VirtualBox driver offers three access methods, selected at init time by `DriverInitParams.virtualbox`. MachineDebugger and FDP name the VM with `common.vm_name`; VBoxVMI names it by its socket. +VirtualBox has one EFER register: `X86Registers::efer` and `msr_efer` both read it, and a register +write with MachineDebugger or VBoxVMI fails (before writing anything) unless they are equal. + | Access method | Cargo feature | Selected by | |---------------|---------------|-------------| | VBoxVMI | `virtualbox-vmi` | `VBoxInitParams::VBoxVMI { socket_path }` | @@ -22,7 +25,7 @@ Pauses are not counted, also across clients: resume resumes the VM even if the G 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. A VM paused through VBoxVMI stays paused if the tool exits or disconnects without resuming it ([#287](https://github.com/Wenzel/libmicrovmi/issues/287)). A register read or write is one request, served by the vCPU itself between two of its instructions: a read is a consistent snapshot of that vCPU and a write changes all its registers at once, also while the VM runs (pause first to read or write several vCPUs at the same point). -The MSRs of `X86Registers` (EFER, STAR, LSTAR, SYSENTER_CS/ESP/EIP, APIC base) are included; an MSR the guest CPU doesn't have reads as 0. `efer` and `msr_efer` are both EFER: a write needs them equal. +The MSRs of `X86Registers` (EFER, STAR, LSTAR, SYSENTER_CS/ESP/EIP, APIC base) are included; an MSR the guest CPU doesn't have reads as 0. 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). diff --git a/src/api/registers.rs b/src/api/registers.rs index 5546b4e2..10517290 100644 --- a/src/api/registers.rs +++ b/src/api/registers.rs @@ -81,7 +81,8 @@ pub struct X86Registers { pub msr_star: u64, ///Used to set the handler for SYSCALL and /or SYSRET instructions used for system calls. Its index value is 0xc0000082. pub msr_lstar: u64, - ///Extened Feature Enable Register + ///Extened Feature Enable Register. The VirtualBox driver has one EFER: `efer` and `msr_efer` + ///read the same value and must be equal in a write. pub efer: u64, ///Advanced Programmable Interrupt Control Register pub apic_base: u64, diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 923f3c9c..fb98df2b 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -10,3 +10,29 @@ pub mod virtualbox_api; pub mod virtualbox_vmi; #[cfg(feature = "xen")] pub mod xen; + +/// VirtualBox has one EFER register, which `X86Registers::efer` and `msr_efer` both are: a register +/// write giving them different values is refused by both VirtualBox access methods. +#[cfg(any(feature = "virtualbox-api", feature = "virtualbox-vmi"))] +#[derive(thiserror::Error, Debug)] +#[error( + "VirtualBox driver: efer {efer:#x} and msr_efer {msr_efer:#x} are both EFER and must be equal" +)] +pub struct EferMismatch { + pub efer: u64, + pub msr_efer: u64, +} + +#[cfg(any(feature = "virtualbox-api", feature = "virtualbox-vmi"))] +impl EferMismatch { + /// Checks a register set to be written. + fn check(r: &crate::api::registers::X86Registers) -> Result<(), Self> { + if r.efer == r.msr_efer { + return Ok(()); + } + Err(EferMismatch { + efer: r.efer, + msr_efer: r.msr_efer, + }) + } +} diff --git a/src/driver/virtualbox_api.rs b/src/driver/virtualbox_api.rs index 83784178..03b6d56e 100644 --- a/src/driver/virtualbox_api.rs +++ b/src/driver/virtualbox_api.rs @@ -8,6 +8,7 @@ use vboxapi::Vm; use crate::api::params::DriverInitParams; use crate::api::registers::{Registers, SegmentReg, SystemTableReg, X86Registers}; use crate::api::{DriverType, Introspectable, PAGE_SIZE}; +use crate::driver::EferMismatch; #[derive(thiserror::Error, Debug)] pub enum VBoxApiDriverError { @@ -23,7 +24,8 @@ pub enum VBoxApiDriverError { } /// `X86Registers` fields as DBGF register names (`IMachineDebugger` register names) and values. -/// Segment selectors are the bare segment names (`cs`). +/// Segment selectors are the bare segment names (`cs`). `efer` is left out: it must equal +/// `msr_efer`, the same DBGF register. fn dbgf_registers(r: &X86Registers) -> Vec<(&'static str, u64)> { let mut regs = vec![ ("rax", r.rax), @@ -54,7 +56,6 @@ fn dbgf_registers(r: &X86Registers) -> Vec<(&'static str, u64)> { ("efer", r.msr_efer), ("star", r.msr_star), ("lstar", r.msr_lstar), - ("efer", r.efer), ("apic_base", r.apic_base), ("gdtr_base", r.gdt.base), ("gdtr_lim", r.gdt.limit.into()), @@ -231,9 +232,10 @@ impl Introspectable for VBoxApi { /// so unchanged control registers and MSRs are not reloaded. Segment selectors, bases and /// limits are written as raw debugger state. Pause the VM first: on a running VM the /// registers are not written all at once. Not atomic: on error, the registers before the - /// failing one stay written. + /// failing one stay written. `efer` and `msr_efer` are both EFER: they must be equal. fn write_registers(&self, vcpu: u16, reg: Registers) -> Result<(), Box> { let Registers::X86(new) = reg; + EferMismatch::check(&new)?; let Registers::X86(old) = self.read_registers(vcpu)?; let mut changed = Vec::new(); for ((name, value), (_, old_value)) in diff --git a/src/driver/virtualbox_vmi.rs b/src/driver/virtualbox_vmi.rs index dff578f8..841c7579 100644 --- a/src/driver/virtualbox_vmi.rs +++ b/src/driver/virtualbox_vmi.rs @@ -7,6 +7,7 @@ use vboxvmi::{Client, Segment, TableRegister}; use crate::api::registers::{Registers, SegmentReg, SystemTableReg, X86Registers}; use crate::api::{DriverType, Introspectable}; +use crate::driver::EferMismatch; #[derive(thiserror::Error, Debug)] pub enum VBoxVmiDriverError { @@ -14,8 +15,6 @@ pub enum VBoxVmiDriverError { NotImplemented(&'static str), #[error("VirtualBox driver, VBoxVMI method: guest physical page {addr:#x} is unreadable (VirtualBox status {rc})")] Unreadable { addr: u64, rc: i32 }, - #[error("VirtualBox driver, VBoxVMI method: efer {efer:#x} and msr_efer {msr_efer:#x} are both EFER and must be equal")] - EferMismatch { efer: u64, msr_efer: u64 }, } fn not_implemented(what: &'static str) -> Result> { @@ -183,13 +182,7 @@ impl Introspectable for VBoxVmi { /// are both EFER: they must be equal. fn write_registers(&self, vcpu: u16, reg: Registers) -> Result<(), Box> { let Registers::X86(r) = reg; - if r.efer != r.msr_efer { - return Err(VBoxVmiDriverError::EferMismatch { - efer: r.efer, - msr_efer: r.msr_efer, - } - .into()); - } + EferMismatch::check(&r)?; let regs = vboxvmi::X86Registers { rax: r.rax, rcx: r.rcx, diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index 9b89c8c9..aae2f17b 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -760,10 +760,23 @@ inventory::submit!(IntegrationTest { }); /// A register write reaches the vCPU itself, not only VirtualBox's copy of its state: SYSENTER_ESP -/// and SYSENTER_EIP (unused by an idle 64-bit Linux guest) written while paused keep their new -/// values after the guest ran, and the guest keeps running. The guard restores them. +/// and SYSENTER_EIP (set once per CPU at bring-up, then unused by an idle 64-bit Linux guest) +/// written while paused keep their new values after the guest ran, and the guest keeps running. +/// The guard then restores only them: the other registers moved on while the guest ran. fn write_registers_survive_resume(drv: Box) { + // a cold-booted guest (VBOX_TEST_CPUS, VBOX_TEST_MEMORY_MB) may not have brought up the vCPU + let last = drv.get_vcpu_count().unwrap() - 1; + for _ in 0..60 { + if x86_registers(drv.as_ref(), last).sysenter_eip != 0 { + break; + } + sleep(Duration::from_millis(500)); + } let mut restore = RegisterRestore::new(drv); + assert_ne!( + restore.original.sysenter_eip, 0, + "the guest never set SYSENTER_EIP" + ); let vcpu = restore.vcpu; let mut changed = restore.original.clone(); changed.sysenter_esp ^= 8; @@ -774,9 +787,15 @@ fn write_registers_survive_resume(drv: Box) { .unwrap(); restore.drv.resume().unwrap(); sleep(Duration::from_secs(1)); - assert_eq!(vm_info("VMState"), "running"); + let state = vm_info("VMState"); restore.drv.pause().unwrap(); let after = x86_registers(restore.drv.as_ref(), vcpu); + restore.original = X86Registers { + sysenter_esp: restore.original.sysenter_esp, + sysenter_eip: restore.original.sysenter_eip, + ..after.clone() + }; + assert_eq!(state, "running"); assert_eq!( (after.sysenter_esp, after.sysenter_eip), (changed.sysenter_esp, changed.sysenter_eip)