Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions doc/src/developer/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions doc/src/reference/drivers/virtualbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
30 changes: 21 additions & 9 deletions src/driver/virtualbox_vmi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(what: &'static str) -> Result<T, Box<dyn Error>> {
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 {
Expand Down Expand Up @@ -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<u16, Box<dyn Error>> {
Ok(self.vcpu_count)
Expand Down Expand Up @@ -115,8 +110,25 @@ impl Introspectable for VBoxVmi {
Ok(())
}

fn write_physical(&self, _paddr: u64, _buf: &[u8]) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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<u64, Box<dyn Error>> {
Expand Down
21 changes: 18 additions & 3 deletions tests/tests/virtualbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>)],
Expand All @@ -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"]);
}
}

Expand Down Expand Up @@ -385,10 +393,17 @@ fn zero_pages(core: &Core, ranges: &[Range<u64>]) -> Vec<u64> {
/// on success and during unwinding. `VBOX_TEST_SEED` replays a pattern.
fn write_physical_matches_core(drv: Box<dyn Introspectable>) {
let vm = vm_name();
let _resume = ResumeVm;
vboxmanage(&["controlvm", &vm, "pause"]);
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<u8>)> = page_addrs
.iter()
.map(|&page| (page, before.ram(page, PAGE)))
Expand Down
Loading