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 = "2b71bb814ac7dc8948e1ca90bec88755f362c69c", optional = true }
vboxvmi = { git = "https://github.com/Wenzel/rs-vboxvmi", rev = "5452496d5f26dc519e7aea39dc3c58a3658557dc", optional = true }
winapi = { version = "0.3", features = [
"tlhelp32",
"winnt",
Expand Down
17 changes: 9 additions & 8 deletions doc/src/developer/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
11 changes: 9 additions & 2 deletions doc/src/reference/drivers/virtualbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }` |
Expand All @@ -14,15 +17,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.
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

Expand Down
3 changes: 2 additions & 1 deletion src/api/registers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
}
8 changes: 5 additions & 3 deletions src/driver/virtualbox_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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<dyn Error>> {
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
Expand Down
144 changes: 137 additions & 7 deletions src/driver/virtualbox_vmi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ 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};
use crate::driver::EferMismatch;

#[derive(thiserror::Error, Debug)]
pub enum VBoxVmiDriverError {
Expand All @@ -20,6 +21,36 @@ fn not_implemented<T>(what: &'static str) -> Result<T, Box<dyn Error>> {
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<Client>,
Expand Down Expand Up @@ -47,7 +78,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<u16, Box<dyn Error>> {
Ok(self.vcpu_count)
Expand Down Expand Up @@ -92,12 +123,111 @@ impl Introspectable for VBoxVmi {
Ok(self.max_addr)
}

fn read_registers(&self, _vcpu: u16) -> Result<Registers, Box<dyn Error>> {
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<Registers, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
let Registers::X86(r) = reg;
EferMismatch::check(&r)?;
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(), &regs)?)
}

/// Synchronous and idempotent: one `resume` undoes any number of `pause`s.
Expand Down
Loading
Loading