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
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@
### Changed

- **Breaking:** removed the legacy FDP VirtualBox access method and its icebox/libFDP dependency.
VirtualBox callers must now select `MachineDebugger` or `VBoxVMI` explicitly in Rust, C and
Python; omitting the access method is an error.
- Omitting the VirtualBox access method selects the default one (ADR-0004): the VBoxVMI method at
the VM's default socket, falling back to the MachineDebugger method only if that socket file does
not exist. C `access_method = 0` and an unset Python `virtualbox` mean the default.
- **Breaking:** `VBoxInitParams::VBoxVMI::socket_path` is now `Option<String>`; `None` (C: a null
`socket_path`, Python: `VBoxVMI()`) uses the VM's default socket. `MicrovmiError::AccessMethodRequired`
is replaced by `VBoxVmiSocketPathRequired` (`virtualbox-vmi` without `virtualbox-api`).
- The `virtualbox` Cargo feature now enables both `virtualbox-api` and `virtualbox-vmi` on Unix.
Windows CI still compiles `virtualbox-api`, but neither replacement method currently works at
runtime on Windows: VBoxVMI is Unix-only, and `vboxapi` has no `VBoxCAPI.dll` loader.

### Added

- C API: `microvmi_get_vcpu_count`.
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 @@ -51,7 +51,7 @@ xenforeignmemory = { version = "=0.3.0", optional = true }
xenevtchn = { version = "=0.2.0", optional = true }
xenvmevent-sys = { version = "=0.2.0", optional = true }
kvmi = { version = "=0.5.0", optional = true }
vboxapi = { git = "https://github.com/Wenzel/rs-vboxapi", rev = "5ba8d86d1cc58a5935208588580782a31fb2d27c", optional = true }
vboxapi = { git = "https://github.com/Wenzel/rs-vboxapi", rev = "fcd2e49a8fdf7802a0fca9b58ec97e72db98e811", optional = true }
vboxvmi = { git = "https://github.com/Wenzel/rs-vboxvmi", rev = "9a349ca535b612232a0d0b26031fedadd3d0ecb0", optional = true }
winapi = { version = "0.3", features = [
"tlhelp32",
Expand Down
21 changes: 16 additions & 5 deletions doc/src/developer/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ configured `cpus`.
- `VBOX_APP_HOME`: VirtualBox bin directory (`VBoxManage`, `VBoxHeadless`, C API library)
- `VBOX_USER_HOME`: VirtualBox configuration directory holding the VM, if not the default one
- `VBOX_TEST_VM` (default `microvmi-alpine`), `VBOX_TEST_SNAPSHOT` (default `base`)
- `VBOX_TEST_METHOD`: `machinedebugger` or `vboxvmi`; unset selects an access method compiled into
the test binary. With `vboxvmi`, setup enables VBoxVMI on the VM and teardown restores the prior
`VBoxInternal/VMI/Enabled` value. The driver honors `VBoxInternal/VMI/SocketPath`; otherwise it
derives the socket from VirtualBox's user home (`VBOX_USER_HOME`, legacy `~/.VirtualBox`, or
`$XDG_CONFIG_HOME/VirtualBox` / `~/.config/VirtualBox`).
- `VBOX_TEST_METHOD`: `machinedebugger` or `vboxvmi`, or the driver's default access method with
VBoxVMI enabled (`default`) or not (`default-fallback`); unset selects an access method compiled
into the test binary. With `vboxvmi` and `default`, setup enables VBoxVMI on the VM and teardown
restores the prior `VBoxInternal/VMI/Enabled` value. The driver honors
`VBoxInternal/VMI/SocketPath`; otherwise it derives the socket from VirtualBox's user home
(`VBOX_USER_HOME`, legacy `~/.VirtualBox`, or `$XDG_CONFIG_HOME/VirtualBox` / `~/.config/VirtualBox`).
- `VBOX_TEST_MEMORY_MB`: discard the snapshot's saved state and cold boot the guest with this much RAM
(e.g. `5120`, to test above 4 GiB). Teardown restores the snapshot, which restores the VM's RAM size.
- `VBOX_TEST_CPUS`: same, with this many vCPUs. The multi-vCPU checks (vCPU count, pause stopping
Expand Down Expand Up @@ -90,6 +91,16 @@ 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.

`virtualbox_default_method_selection` (`virtualbox` feature) checks the default access method under
any `VBOX_TEST_METHOD`: with VBoxVMI enabled, the context's driver holds the socket and a second
default init must fail as busy instead of falling back; with it disabled, the socket is missing, the
driver (MachineDebugger) reads memory, and an explicit VBoxVMI without a socket path must fail. Run it
under each selection:

~~~
for m in default default-fallback vboxvmi machinedebugger; do MICROVMI_TEST_FILTER=virtualbox_default_method_selection VBOX_TEST_METHOD=$m RUST_LOG=microvmi=info cargo test --features virtualbox --test integration; done
~~~

### MachineDebugger/VBoxVMI cross-method consistency

The `cross_method_consistency` inventory test opens MachineDebugger and VBoxVMI
Expand Down
9 changes: 7 additions & 2 deletions doc/src/reference/api/python_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ micro = Microvmi(DriverType.KVM, init_params)
### Selecting the VirtualBox access method

`init_params.virtualbox` takes a `VBoxInitParamsPy`, the equivalent of the Rust `VBoxInitParams`.
VirtualBox initialization requires one of its two access methods explicitly.
Left unset, the driver uses its default access method: VBoxVMI at the VM's default socket, or
MachineDebugger if VBoxVMI is not enabled on the VM (bindings built with the `virtualbox` feature).

~~~Python
from microvmi import Microvmi, DriverType, DriverInitParamsPy, CommonInitParamsPy, VBoxInitParamsPy
Expand All @@ -60,9 +61,13 @@ common.vm_name = "windows10"
# setup main init_params
init_params = DriverInitParamsPy()
init_params.common = common
# MachineDebugger access method (requires the bindings built with the virtualbox-api feature)
# default access method
micro = Microvmi(DriverType.VirtualBox, init_params)
# or the MachineDebugger access method (requires the bindings built with the virtualbox-api feature)
init_params.virtualbox = VBoxInitParamsPy.MachineDebugger()
# or the VBoxVMI access method (virtualbox-vmi feature), which names the VM by its socket
init_params.virtualbox = VBoxInitParamsPy.VBoxVMI(socket_path="/home/user/.config/VirtualBox/vmi/<VM UUID>.sock")
# or the VM's default VBoxVMI socket (virtualbox feature)
init_params.virtualbox = VBoxInitParamsPy.VBoxVMI()
micro = Microvmi(DriverType.VirtualBox, init_params)
~~~
28 changes: 21 additions & 7 deletions doc/src/reference/drivers/virtualbox.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# VirtualBox

The VirtualBox driver offers two access methods, selected explicitly at init time by
The VirtualBox driver offers two access methods, selected at init time by
`DriverInitParams.virtualbox`. MachineDebugger names the VM with `common.vm_name`; VBoxVMI names it
by its socket. The `virtualbox` Cargo feature enables both methods on Unix; the method-specific
features remain available separately.
by its socket, or by `common.vm_name` for the VM's default socket. The `virtualbox` Cargo feature
enables both methods on Unix; the method-specific features remain available separately.

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.
Expand All @@ -13,6 +13,19 @@ write with MachineDebugger or VBoxVMI fails (before writing anything) unless the
| VBoxVMI | `virtualbox-vmi` or `virtualbox` | `VBoxInitParams::VBoxVMI { socket_path }` |
| MachineDebugger | `virtualbox-api` or `virtualbox` | `VBoxInitParams::MachineDebugger` |

## Default access method

Without `DriverInitParams.virtualbox`, the driver uses the VBoxVMI method at the VM's default socket
(`<VirtualBox user home>/vmi/<VM UUID>.sock`, or the VM's `VBoxInternal/VMI/SocketPath`, both
resolved by `common.vm_name` through the VirtualBox API), and falls back to the MachineDebugger
method only if that socket file does not exist, i.e. VBoxVMI is not enabled on the VM. Any other
VBoxVMI error (another client connected, permission refused, protocol mismatch) is returned. An
explicitly selected method never falls back. The choice, and the reason for a fallback, are logged at
`info` level (`RUST_LOG=microvmi=info`). See [ADR-0004](https://github.com/Wenzel/libmicrovmi/blob/dev/docs/adr/0004-default-vbox-access-method.md).

With one method compiled in: `virtualbox-api` only uses MachineDebugger by default;
`virtualbox-vmi` only needs an explicit `socket_path`.

## VBoxVMI method

Speaks VBoxVMI, a protocol served from inside the VM process of a patched VirtualBox over a per-VM Unix socket.
Expand Down Expand Up @@ -43,10 +56,11 @@ Segment selectors, bases and limits are written as raw state: no descriptor tabl

### Initialization parameters

- `vm_name`: ignored (the socket belongs to one VM)
- `virtualbox`: `VBoxInitParams::VBoxVMI { socket_path }`
- Python: `init_params.virtualbox = VBoxInitParamsPy.VBoxVMI(socket_path="...")`
- C: `.virtualbox = { .access_method = VBOX_ACCESS_METHOD_VBOXVMI, .socket_path = "..." }` (see `c_examples/vbox-read-page.c`)
- `vm_name`: required when `socket_path` is omitted, ignored otherwise (the socket belongs to one VM)
- `virtualbox`: `VBoxInitParams::VBoxVMI { socket_path: Some(path) }`, or `socket_path: None` for the
VM's default socket (needs the `virtualbox` feature: the socket is resolved through the VirtualBox API)
- Python: `init_params.virtualbox = VBoxInitParamsPy.VBoxVMI(socket_path="...")`, or `VBoxVMI()`
- C: `.virtualbox = { .access_method = VBOX_ACCESS_METHOD_VBOXVMI, .socket_path = "..." }` (see `c_examples/vbox-read-page.c`), or a null `socket_path`

The examples take `--vbox_vmi <socket>`:
~~~
Expand Down
9 changes: 7 additions & 2 deletions doc/src/reference/integration/volatility3.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ To pass the KVMi socket:

vmi:///?vm_name=windows10&kvm_unix_socket=/tmp/introspector

VirtualBox without `vbox_access_method` uses the driver's default access method (VBoxVMI if the VM
serves its socket, else MachineDebugger):

vmi://VirtualBox/?vm_name=windows10

To use VirtualBox's MachineDebugger access method:

vmi://VirtualBox/?vm_name=windows10&vbox_access_method=machinedebugger

To use VirtualBox's VBoxVMI access method:
To use VirtualBox's VBoxVMI access method (the VM's default socket without `vbox_socket_path`):

vmi://VirtualBox/?vbox_access_method=vboxvmi&vbox_socket_path=/home/user/.config/VirtualBox/vmi/<VM UUID>.sock

Expand All @@ -48,7 +53,7 @@ URL parameters:
| `vm_name` | Name of the VM |
| `kvm_unix_socket` | KVMi UNIX socket |
| `memflow_connector_name` | memflow connector name |
| `vbox_access_method` | VirtualBox access method: `machinedebugger` or `vboxvmi` |
| `vbox_access_method` | VirtualBox access method: `machinedebugger` or `vboxvmi`; default if omitted |
| `vbox_socket_path` | VBoxVMI socket, for `vbox_access_method=vboxvmi` |

## Running volatility3
Expand Down
33 changes: 33 additions & 0 deletions docs/adr/0004-default-vbox-access-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Default VirtualBox access method

Supersedes the "no default access method" part of [ADR-0003](0003-drop-fdp-access-method.md).

## Context

ADR-0003 made VirtualBox callers name an **Access method**, because the only default left was an
implicit fallback to FDP. LibVMI now reaches VirtualBox through libmicrovmi: requiring an explicit
method makes every LibVMI user know whether their VM serves a **VBoxVMI** socket, and a
zero-initialized C params struct fails.

## Decision

When no access method is selected, the VirtualBox **Driver** uses the **VBoxVMI method** at the VM's
default socket (`<VirtualBox user home>/vmi/<VM UUID>.sock`, or the VM's
`VBoxInternal/VMI/SocketPath` extradata), and falls back to the **MachineDebugger method** only when
that socket file does not exist. VBoxVMI selected without a socket path uses that same default
socket. The socket is resolved through the VirtualBox API, so both features are needed: with
`virtualbox-vmi` only, the default and a VBoxVMI selection without a path are errors; with
`virtualbox-api` only, the default is the MachineDebugger method.

An explicitly selected method never falls back. Any other VBoxVMI failure (busy socket, permission
refusal, protocol mismatch, handshake timeout) is returned, not hidden behind the slower method. The
Driver logs the method it picked, and why it fell back, at `info` level; there is no API to query
it. There is no `Auto` variant: "no VirtualBox params" is the default.

## Consequences

- C: `access_method = 0` means the default; `VBOX_ACCESS_METHOD_VBOXVMI` with a null `socket_path`
means the default socket. The struct layout is unchanged.
- Rust: `VBoxInitParams::VBoxVMI::socket_path` becomes `Option<String>`.
- Python: an unset `virtualbox` means the default; `VBoxInitParamsPy.VBoxVMI()` takes no path.
- A VM with VBoxVMI enabled but whose socket is busy fails instead of using MachineDebugger.
4 changes: 3 additions & 1 deletion python/microvmi/microvmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ def __init__(
# init
m = Microvmi(None, init_params)

# VirtualBox: select the MachineDebugger access method
# VirtualBox: the default access method (VBoxVMI if the VM serves its socket, else
# MachineDebugger), or select one explicitly
m = Microvmi(DriverType.VirtualBox, init_params)
init_params.virtualbox = VBoxInitParamsPy.MachineDebugger()
m = Microvmi(DriverType.VirtualBox, init_params)
"""
Expand Down
4 changes: 0 additions & 4 deletions python/microvmi/volatility/vmi_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ def url_to_driver_parameters(url: str) -> Tuple[Optional[DriverType], Optional[D
driver_type: Optional[DriverType] = _parse_hypervisor(parsed_url.netloc)
# init params
init_params: Optional[DriverInitParamsPy] = _parse_driver_init_params(parsed_url.query)
if driver_type == DriverType.VirtualBox and (init_params is None or init_params.virtualbox is None):
raise MicrovmiHandlerError("VirtualBox requires vbox_access_method=machinedebugger or vboxvmi")
return driver_type, init_params


Expand Down Expand Up @@ -132,8 +130,6 @@ def _parse_driver_init_params(query: str) -> Optional[DriverInitParamsPy]:
if vbox_access_method == "machinedebugger":
virtualbox = VBoxInitParamsPy.MachineDebugger()
elif vbox_access_method == "vboxvmi":
if vbox_socket_path is None:
raise MicrovmiHandlerError("vbox_access_method=vboxvmi requires vbox_socket_path")
virtualbox = VBoxInitParamsPy.VBoxVMI(socket_path=vbox_socket_path)
elif vbox_access_method is not None:
raise MicrovmiHandlerError(
Expand Down
11 changes: 8 additions & 3 deletions python/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ impl MemflowInitParamsPy {

/// equivalent of `VBoxInitParams` for Python: selects the VirtualBox access method
///
/// Usage from Python: `VBoxInitParamsPy.MachineDebugger()` or
/// `VBoxInitParamsPy.VBoxVMI(socket_path="/path/to/vm.sock")`
/// Usage from Python: `VBoxInitParamsPy.MachineDebugger()`,
/// `VBoxInitParamsPy.VBoxVMI(socket_path="/path/to/vm.sock")`, or `VBoxInitParamsPy.VBoxVMI()`
/// for the VM's default socket. Leave `DriverInitParamsPy.virtualbox` unset for the default
/// access method.
#[pyclass]
#[derive(Debug, Clone)]
pub enum VBoxInitParamsPy {
// struct variant (not unit) so variants with fields can be added without changing the Python API
MachineDebugger {},
VBoxVMI { socket_path: String },
#[pyo3(constructor = (socket_path = None))]
VBoxVMI {
socket_path: Option<String>,
},
}

/// equivalent of `DriverInitParams` for Python
Expand Down
15 changes: 8 additions & 7 deletions python/tests/unit/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ def test_parse_hypervisor_none():
@pytest.mark.parametrize("drv_type_enum_member", [d for d in DriverType])
def test_parse_hypervisor_each(drv_type_enum_member):
expected = drv_type_enum_member
query = "?vbox_access_method=machinedebugger" if expected == DriverType.VirtualBox else ""
url = f"vmi://{drv_type_enum_member.name}/vm_name{query}"
url = f"vmi://{drv_type_enum_member.name}/vm_name"
drv_type, _ = url_to_driver_parameters(url)
assert expected == drv_type

Expand Down Expand Up @@ -72,9 +71,9 @@ def test_parse_init_param_vbox_access_method():
assert isinstance(init_params.virtualbox, VBoxInitParamsPy.MachineDebugger)


def test_parse_vbox_requires_access_method():
with pytest.raises(MicrovmiHandlerError, match="requires vbox_access_method"):
url_to_driver_parameters("vmi://VirtualBox/?vm_name=windows10")
def test_parse_vbox_default_access_method():
_, init_params = url_to_driver_parameters("vmi://VirtualBox/?vm_name=windows10")
assert init_params.virtualbox is None


def test_parse_init_param_vbox_vboxvmi():
Expand All @@ -85,9 +84,11 @@ def test_parse_init_param_vbox_vboxvmi():


def test_parse_init_param_vbox_vboxvmi_no_socket():
# VBoxVMI at the VM's default socket
url = "vmi:///?vbox_access_method=vboxvmi"
with pytest.raises(MicrovmiHandlerError):
url_to_driver_parameters(url)
_, init_params = url_to_driver_parameters(url)
assert isinstance(init_params.virtualbox, VBoxInitParamsPy.VBoxVMI)
assert init_params.virtualbox.socket_path is None


def test_parse_init_param_vbox_socket_requires_vboxvmi():
Expand Down
13 changes: 9 additions & 4 deletions src/api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,20 @@ pub struct MemflowInitParams {

/// VirtualBox initialization parameters: selects one of the two access methods.
///
/// MachineDebugger names the VM by `common.vm_name`; VBoxVMI names it by its socket. VirtualBox
/// initialization requires this parameter.
/// MachineDebugger names the VM by `common.vm_name`; VBoxVMI names it by its socket, or by
/// `common.vm_name` when `socket_path` is omitted. Without this parameter, the VirtualBox driver
/// uses the VBoxVMI method at the VM's default socket, and falls back to the MachineDebugger
/// method only if that socket file does not exist.
#[derive(Debug, Clone, PartialEq)]
pub enum VBoxInitParams {
/// MachineDebugger access method (`virtualbox-api` or `virtualbox` feature):
/// VirtualBox's `IMachineDebugger` API
MachineDebugger,
/// VBoxVMI access method (`virtualbox-vmi` or `virtualbox` feature): the VM's VBoxVMI socket
VBoxVMI { socket_path: String },
/// VBoxVMI access method (`virtualbox-vmi` or `virtualbox` feature): the VM's VBoxVMI socket.
/// `None`: the VM's default socket, `<VirtualBox user home>/vmi/<VM UUID>.sock` or the VM's
/// `VBoxInternal/VMI/SocketPath` extradata (`virtualbox` feature only: resolved through the
/// VirtualBox API).
VBoxVMI { socket_path: Option<String> },
}

/// Common initialization parameters
Expand Down
19 changes: 19 additions & 0 deletions src/capi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,25 @@ pub unsafe extern "C" fn microvmi_get_max_physical_addr(
}
}

#[allow(clippy::missing_safety_doc)]
#[no_mangle]
pub unsafe extern "C" fn microvmi_get_vcpu_count(
context: *mut c_void,
count_ptr: *mut uint16_t,
) -> bool {
if context.is_null() {
return false;
}
let driver = get_driver_mut_ptr(context);
match (*driver).get_vcpu_count() {
Ok(count) => {
count_ptr.write(count);
true
}
Err(_) => false,
}
}

#[allow(clippy::missing_safety_doc)]
#[no_mangle]
pub unsafe extern "C" fn microvmi_read_registers(
Expand Down
Loading
Loading