diff --git a/CHANGELOG.md b/CHANGELOG.md index 380489ae..b8f936cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`; `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`. diff --git a/Cargo.lock b/Cargo.lock index 47051856..4e8a0bb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "vboxapi" version = "0.1.0" -source = "git+https://github.com/Wenzel/rs-vboxapi?rev=5ba8d86d1cc58a5935208588580782a31fb2d27c#5ba8d86d1cc58a5935208588580782a31fb2d27c" +source = "git+https://github.com/Wenzel/rs-vboxapi?rev=fcd2e49a8fdf7802a0fca9b58ec97e72db98e811#fcd2e49a8fdf7802a0fca9b58ec97e72db98e811" dependencies = [ "libloading 0.8.6", "log", diff --git a/Cargo.toml b/Cargo.toml index f3dbc756..341254a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/doc/src/developer/tests.md b/doc/src/developer/tests.md index 50831155..1b1d4fe9 100644 --- a/doc/src/developer/tests.md +++ b/doc/src/developer/tests.md @@ -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 @@ -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 diff --git a/doc/src/reference/api/python_api.md b/doc/src/reference/api/python_api.md index 3acccebb..cf372ca1 100644 --- a/doc/src/reference/api/python_api.md +++ b/doc/src/reference/api/python_api.md @@ -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 @@ -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/.sock") +# or the VM's default VBoxVMI socket (virtualbox feature) +init_params.virtualbox = VBoxInitParamsPy.VBoxVMI() micro = Microvmi(DriverType.VirtualBox, init_params) ~~~ diff --git a/doc/src/reference/drivers/virtualbox.md b/doc/src/reference/drivers/virtualbox.md index 1f56b695..73b027e6 100644 --- a/doc/src/reference/drivers/virtualbox.md +++ b/doc/src/reference/drivers/virtualbox.md @@ -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. @@ -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 +(`/vmi/.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. @@ -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 `: ~~~ diff --git a/doc/src/reference/integration/volatility3.md b/doc/src/reference/integration/volatility3.md index 242c2a23..ca9e4cd8 100644 --- a/doc/src/reference/integration/volatility3.md +++ b/doc/src/reference/integration/volatility3.md @@ -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/.sock @@ -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 diff --git a/docs/adr/0004-default-vbox-access-method.md b/docs/adr/0004-default-vbox-access-method.md new file mode 100644 index 00000000..3791df76 --- /dev/null +++ b/docs/adr/0004-default-vbox-access-method.md @@ -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 (`/vmi/.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`. +- 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. diff --git a/python/microvmi/microvmi.py b/python/microvmi/microvmi.py index da37c7f2..86ab5a38 100644 --- a/python/microvmi/microvmi.py +++ b/python/microvmi/microvmi.py @@ -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) """ diff --git a/python/microvmi/volatility/vmi_handler.py b/python/microvmi/volatility/vmi_handler.py index 6f6a5edb..b919f717 100644 --- a/python/microvmi/volatility/vmi_handler.py +++ b/python/microvmi/volatility/vmi_handler.py @@ -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 @@ -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( diff --git a/python/src/params.rs b/python/src/params.rs index 99cdf9e9..c554a602 100644 --- a/python/src/params.rs +++ b/python/src/params.rs @@ -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, + }, } /// equivalent of `DriverInitParams` for Python diff --git a/python/tests/unit/test_handler.py b/python/tests/unit/test_handler.py index fb51edc0..ebd4fd31 100644 --- a/python/tests/unit/test_handler.py +++ b/python/tests/unit/test_handler.py @@ -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 @@ -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(): @@ -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(): diff --git a/src/api/params.rs b/src/api/params.rs index 756522de..d0ab158b 100644 --- a/src/api/params.rs +++ b/src/api/params.rs @@ -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, `/vmi/.sock` or the VM's + /// `VBoxInternal/VMI/SocketPath` extradata (`virtualbox` feature only: resolved through the + /// VirtualBox API). + VBoxVMI { socket_path: Option }, } /// Common initialization parameters diff --git a/src/capi/mod.rs b/src/capi/mod.rs index 7c82efdc..7f53d63f 100644 --- a/src/capi/mod.rs +++ b/src/capi/mod.rs @@ -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( diff --git a/src/capi/params.rs b/src/capi/params.rs index 4fe4209a..fac54bb8 100644 --- a/src/capi/params.rs +++ b/src/capi/params.rs @@ -41,8 +41,8 @@ pub struct MemflowInitParamsFFI { pub connector_args: MemflowConnectorParamsFFI, } -// `access_method = 0` means no VirtualBox parameters. Selecting the VirtualBox driver with this -// value is an error. +// `access_method = 0` means no VirtualBox parameters: the VirtualBox driver then uses its default +// access method. /// `VBoxInitParamsFFI.access_method`: `VBoxInitParams::MachineDebugger` pub const VBOX_ACCESS_METHOD_MACHINE_DEBUGGER: u32 = 1; /// `VBoxInitParamsFFI.access_method`: `VBoxInitParams::VBoxVMI`, with `VBoxInitParamsFFI.socket_path` @@ -50,14 +50,15 @@ pub const VBOX_ACCESS_METHOD_VBOXVMI: u32 = 2; /// equivalent of `Option` with C compatibility. /// -/// A zero-initialized struct has no VirtualBox parameters. +/// A zero-initialized struct has no VirtualBox parameters: the default access method. #[repr(C)] #[derive(Debug, Clone)] pub struct VBoxInitParamsFFI { - /// one of the `VBOX_ACCESS_METHOD_*` constants, or zero for no VirtualBox parameters + /// one of the `VBOX_ACCESS_METHOD_*` constants, or zero for the default access method // plain integer, not a Rust enum: an out-of-range value from C would be UB in an enum pub access_method: u32, - /// `VBOX_ACCESS_METHOD_VBOXVMI`: the VM's VBoxVMI socket path (mandatory); ignored otherwise + /// `VBOX_ACCESS_METHOD_VBOXVMI`: the VM's VBoxVMI socket path, or null for the VM's default + /// socket; ignored otherwise pub socket_path: *mut c_char, } @@ -140,12 +141,12 @@ impl TryFrom for DriverInitParams { VBOX_ACCESS_METHOD_MACHINE_DEBUGGER => Some(VBoxInitParams::MachineDebugger), VBOX_ACCESS_METHOD_VBOXVMI => { let path = value.virtualbox.socket_path; - if path.is_null() { - return Err("VBOX_ACCESS_METHOD_VBOXVMI requires a socket_path".into()); - } - Some(VBoxInitParams::VBoxVMI { - socket_path: unsafe { CStr::from_ptr(path) }.to_owned().into_string()?, - }) + let socket_path = if path.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(path) }.to_owned().into_string()?) + }; + Some(VBoxInitParams::VBoxVMI { socket_path }) } other => return Err(format!("unknown VirtualBox access method: {}", other).into()), }; diff --git a/src/driver/virtualbox_api.rs b/src/driver/virtualbox_api.rs index 03b6d56e..be7f666e 100644 --- a/src/driver/virtualbox_api.rs +++ b/src/driver/virtualbox_api.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::convert::TryFrom; use std::error::Error; +#[cfg(feature = "virtualbox-vmi")] +use std::path::PathBuf; use vboxapi::Vm; @@ -98,11 +100,7 @@ pub struct VBoxApi { impl VBoxApi { pub fn new(init_params: DriverInitParams) -> Result> { - let vm_name = init_params - .common - .ok_or(VBoxApiDriverError::MissingVMName)? - .vm_name; - let vm = Vm::attach(&vm_name)?; + let vm = Vm::attach(vm_name(&init_params)?)?; let max_addr = vm .ram_ranges()? .last() @@ -112,6 +110,20 @@ impl VBoxApi { } } +fn vm_name(init_params: &DriverInitParams) -> Result<&str, VBoxApiDriverError> { + Ok(&init_params + .common + .as_ref() + .ok_or(VBoxApiDriverError::MissingVMName)? + .vm_name) +} + +/// The VM's default VBoxVMI socket, resolved through the VirtualBox API. +#[cfg(feature = "virtualbox-vmi")] +pub fn vmi_socket_path(init_params: &DriverInitParams) -> Result> { + Ok(vboxapi::vmi_socket_path(vm_name(init_params)?)?) +} + impl Introspectable for VBoxApi { /// On error, `bytes_read` is the length of the readable prefix, up to the first unreadable page. fn read_physical( diff --git a/src/driver/virtualbox_vmi.rs b/src/driver/virtualbox_vmi.rs index 681fd309..af1abf7a 100644 --- a/src/driver/virtualbox_vmi.rs +++ b/src/driver/virtualbox_vmi.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::convert::TryFrom; use std::error::Error; +use std::path::Path; use vboxvmi::{Client, Segment, TableRegister}; @@ -58,7 +59,7 @@ pub struct VBoxVmi { impl VBoxVmi { /// Connects to the VM's VBoxVMI socket and performs the handshake. - pub fn new(socket_path: &str) -> Result> { + pub fn new(socket_path: &Path) -> Result> { let mut client = Client::connect(socket_path)?; let max_addr = client .ram_ranges()? @@ -74,6 +75,14 @@ impl VBoxVmi { } } +/// Whether `e`, from [`VBoxVmi::new`], means the socket file does not exist: VBoxVMI is not +/// enabled on the VM (or it is not running). +#[cfg(feature = "virtualbox-api")] +pub fn is_socket_missing(e: &(dyn Error + 'static)) -> bool { + matches!(e.downcast_ref(), Some(vboxvmi::Error::Connect { source, .. }) + if source.kind() == std::io::ErrorKind::NotFound) +} + impl Introspectable for VBoxVmi { fn get_vcpu_count(&self) -> Result> { Ok(self.vcpu_count) diff --git a/src/errors.rs b/src/errors.rs index 1a33771f..30225272 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -7,8 +7,8 @@ pub enum MicrovmiError { NoDriverAvailable, #[error("driver {0:?} has not been compiled")] DriverNotCompiled(DriverType), - #[error("{0:?} requires an explicit access method")] - AccessMethodRequired(DriverType), + #[error("VirtualBox VBoxVMI method needs a socket path (enable feature virtualbox-api for the VM's default socket)")] + VBoxVmiSocketPathRequired, #[error("{driver:?} access method {method} has not been compiled (enable feature {feature})")] AccessMethodNotCompiled { driver: DriverType, diff --git a/src/lib.rs b/src/lib.rs index dfe247d7..bbd38c41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,13 +142,14 @@ fn init_driver( } } -/// Initialize the VirtualBox driver with the access method selected by `init_params.virtualbox` +/// Initialize the VirtualBox driver with the access method selected by `init_params.virtualbox`, +/// or the default one when it is `None` #[cfg(any(feature = "virtualbox-api", feature = "virtualbox-vmi"))] fn init_virtualbox( init_params: DriverInitParams, ) -> Result, MicrovmiError> { use api::params::VBoxInitParams; - match init_params.virtualbox { + match &init_params.virtualbox { #[cfg(feature = "virtualbox-api")] Some(VBoxInitParams::MachineDebugger) => Ok(Box::new(VBoxApi::new(init_params)?)), #[cfg(not(feature = "virtualbox-api"))] @@ -158,8 +159,16 @@ fn init_virtualbox( feature: "virtualbox-api", }), #[cfg(feature = "virtualbox-vmi")] - Some(VBoxInitParams::VBoxVMI { ref socket_path }) => { - Ok(Box::new(VBoxVmi::new(socket_path)?)) + Some(VBoxInitParams::VBoxVMI { + socket_path: Some(socket_path), + }) => Ok(Box::new(VBoxVmi::new(std::path::Path::new(socket_path))?)), + #[cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] + Some(VBoxInitParams::VBoxVMI { socket_path: None }) => Ok(Box::new(VBoxVmi::new( + &driver::virtualbox_api::vmi_socket_path(&init_params)?, + )?)), + #[cfg(all(not(feature = "virtualbox-api"), feature = "virtualbox-vmi"))] + Some(VBoxInitParams::VBoxVMI { socket_path: None }) | None => { + Err(MicrovmiError::VBoxVmiSocketPathRequired) } #[cfg(not(feature = "virtualbox-vmi"))] Some(VBoxInitParams::VBoxVMI { .. }) => Err(MicrovmiError::AccessMethodNotCompiled { @@ -167,6 +176,28 @@ fn init_virtualbox( method: "VBoxVMI", feature: "virtualbox-vmi", }), - None => Err(MicrovmiError::AccessMethodRequired(DriverType::VirtualBox)), + #[cfg(all(feature = "virtualbox-api", not(feature = "virtualbox-vmi")))] + None => { + info!("VirtualBox: MachineDebugger access method (the only one compiled)"); + Ok(Box::new(VBoxApi::new(init_params)?)) + } + #[cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] + None => { + let socket = driver::virtualbox_api::vmi_socket_path(&init_params)?; + match VBoxVmi::new(&socket) { + Ok(driver) => { + info!("VirtualBox: VBoxVMI access method, socket {:?}", socket); + Ok(Box::new(driver)) + } + Err(e) if driver::virtualbox_vmi::is_socket_missing(&*e) => { + info!( + "VirtualBox: MachineDebugger access method, falling back: no VBoxVMI socket {:?} (VBoxVMI not enabled on the VM)", + socket + ); + Ok(Box::new(VBoxApi::new(init_params)?)) + } + Err(e) => Err(e.into()), + } + } } } diff --git a/tests/common/virtualbox.rs b/tests/common/virtualbox.rs index 27f332d6..bd38b27b 100644 --- a/tests/common/virtualbox.rs +++ b/tests/common/virtualbox.rs @@ -8,10 +8,11 @@ //! - `VBOX_TEST_CPUS`: same, with that many vCPUs //! - `VBOX_TEST_CORE_DIR` (default: the temp dir): where tests write core dumps, as large as the //! guest's RAM (avoid a RAM-backed tmpfs for big guests); deleted at teardown -//! - `VBOX_TEST_METHOD`: the access method, `machinedebugger` or `vboxvmi`. Unset selects an -//! access method compiled into the test binary. For `vboxvmi`, setup enables VBoxVMI on the VM -//! while preserving the prior extradata value. The socket honors `VBoxInternal/VMI/SocketPath`; -//! otherwise it is `/vmi/.sock`. +//! - `VBOX_TEST_METHOD`: the access 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. For `vboxvmi` and `default`, setup enables +//! VBoxVMI on the VM while preserving the prior extradata value. The socket honors +//! `VBoxInternal/VMI/SocketPath`; otherwise it is `/vmi/.sock`. use parking_lot::Mutex; use std::env; use std::fs; @@ -30,47 +31,61 @@ use crate::common::context::Context; /// The VM lifecycle is the same for every access method; only `init_driver` uses `method`. pub struct VirtualBox { - pub method: VBoxInitParams, + /// `None`: the driver's default access method. + pub method: Option, + /// `VBoxInternal/VMI/Enabled` while the VM runs. + pub vmi_enabled: bool, prior_vmi_enabled: Mutex>>, } impl VirtualBox { /// The context for the access method in `VBOX_TEST_METHOD`. pub fn from_env() -> Self { - let method = match env::var("VBOX_TEST_METHOD").as_deref() { - Err(_) => default_method(), - Ok("machinedebugger") => VBoxInitParams::MachineDebugger, - Ok("vboxvmi") => VBoxInitParams::VBoxVMI { - socket_path: vboxvmi_socket(), - }, + match env::var("VBOX_TEST_METHOD").as_deref() { + Err(_) => VirtualBox::new(compiled_method()), + Ok("machinedebugger") => VirtualBox::new(VBoxInitParams::MachineDebugger), + Ok("vboxvmi") => VirtualBox::new(vboxvmi()), + Ok("default") => VirtualBox::with_default_method(true), + Ok("default-fallback") => VirtualBox::with_default_method(false), Ok(other) => panic!("unknown VBOX_TEST_METHOD {:?}", other), - }; - VirtualBox::new(method) + } } - /// The context for `method`. + /// The context for `method`; VBoxVMI is enabled on the VM for the VBoxVMI method. pub fn new(method: VBoxInitParams) -> Self { + let vmi_enabled = matches!(method, VBoxInitParams::VBoxVMI { .. }); + VirtualBox { + method: Some(method), + vmi_enabled, + prior_vmi_enabled: Mutex::new(None), + } + } + + /// The context for the driver's default access method, with VBoxVMI enabled on the VM or not. + pub fn with_default_method(vmi_enabled: bool) -> Self { VirtualBox { - method, + method: None, + vmi_enabled, prior_vmi_enabled: Mutex::new(None), } } +} - fn is_vboxvmi(&self) -> bool { - matches!(self.method, VBoxInitParams::VBoxVMI { .. }) +fn vboxvmi() -> VBoxInitParams { + VBoxInitParams::VBoxVMI { + socket_path: Some(vboxvmi_socket()), } } +/// The access method when `VBOX_TEST_METHOD` is unset: one compiled into the test binary. #[cfg(feature = "virtualbox-api")] -fn default_method() -> VBoxInitParams { +fn compiled_method() -> VBoxInitParams { VBoxInitParams::MachineDebugger } #[cfg(all(not(feature = "virtualbox-api"), feature = "virtualbox-vmi"))] -fn default_method() -> VBoxInitParams { - VBoxInitParams::VBoxVMI { - socket_path: vboxvmi_socket(), - } +fn compiled_method() -> VBoxInitParams { + vboxvmi() } /// The configured or default VBoxVMI socket path. @@ -134,7 +149,7 @@ pub fn try_vboxmanage(args: &[&str]) -> Result { } /// Returns an extradata value, or `None` when the key is unset. -fn extradata(key: &str) -> Option { +pub fn extradata(key: &str) -> Option { let output = vboxmanage(&["getextradata", &vm_name(), key]); output .strip_prefix("Value: ") @@ -195,7 +210,7 @@ impl VirtualBox { pub fn init_params(&self) -> DriverInitParams { DriverInitParams { common: Some(CommonInitParams { vm_name: vm_name() }), - virtualbox: Some(self.method.clone()), + virtualbox: self.method.clone(), ..Default::default() } } @@ -223,7 +238,7 @@ impl Context for VirtualBox { args.extend(modify.iter().map(String::as_str)); vboxmanage(&args); } - if self.is_vboxvmi() { + if self.vmi_enabled { let mut prior = self.prior_vmi_enabled.lock(); assert!( prior.is_none(), @@ -262,7 +277,7 @@ impl Context for VirtualBox { fn teardown(&self) { debug!("teardown test"); power_off_and_restore(); - if self.is_vboxvmi() { + if self.vmi_enabled { let prior = self .prior_vmi_enabled .lock() diff --git a/tests/tests/virtualbox.rs b/tests/tests/virtualbox.rs index a5910af3..93f8fc1e 100644 --- a/tests/tests/virtualbox.rs +++ b/tests/tests/virtualbox.rs @@ -544,6 +544,59 @@ inventory::submit!(IntegrationTest { test_fn: vboxvmi_second_client_is_busy }); +/// The default access method takes VBoxVMI when the VM serves its socket, and MachineDebugger only +/// when the socket file is missing; a busy socket, or an explicit VBoxVMI without the socket, is an +/// error. Meaningful under every `VBOX_TEST_METHOD`: it only relies on whether VBoxVMI is enabled. +#[cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] +fn default_method_selection(drv: Box) { + use crate::common::virtualbox::extradata; + use microvmi::api::params::{CommonInitParams, DriverInitParams, VBoxInitParams}; + use microvmi::api::DriverType; + + let init = |virtualbox| { + microvmi::init( + Some(DriverType::VirtualBox), + Some(DriverInitParams { + common: Some(CommonInitParams { vm_name: vm_name() }), + virtualbox, + ..Default::default() + }), + ) + }; + if extradata("VBoxInternal/VMI/Enabled").as_deref() == Some("1") { + // Fails only if `drv` holds the one VBoxVMI client slot, and the default didn't fall back. + let err = init(None) + .err() + .expect("default init succeeded while the VBoxVMI socket is busy"); + assert!( + err.to_string().contains("already connected"), + "default init: {}", + err + ); + } else { + assert!(!std::path::Path::new(&vboxvmi_socket()).exists()); + let (mut byte, mut read) = ([0u8], 0); + drv.read_physical(0, &mut byte, &mut read) + .expect("MachineDebugger read"); + let err = init(Some(VBoxInitParams::VBoxVMI { socket_path: None })) + .err() + .expect("explicit VBoxVMI without a socket fell back"); + assert!( + err.to_string() + .contains("cannot connect to the VBoxVMI socket"), + "explicit VBoxVMI: {}", + err + ); + } + drop(drv); +} + +#[cfg(all(feature = "virtualbox-api", feature = "virtualbox-vmi"))] +inventory::submit!(IntegrationTest { + name: "virtualbox_default_method_selection", + test_fn: default_method_selection +}); + /// Resumes the VM even if selecting pages or dumping the first core fails. struct ResumeVm; diff --git a/tests/virtualbox_errors.rs b/tests/virtualbox_errors.rs index e0326e2c..e9b7b58c 100644 --- a/tests/virtualbox_errors.rs +++ b/tests/virtualbox_errors.rs @@ -26,19 +26,25 @@ fn init_error( } } -#[cfg(any( - feature = "virtualbox-vmi", - all(feature = "virtualbox-api", not(feature = "kvm")) -))] +/// Without `virtualbox-api`, nothing resolves the VM's default VBoxVMI socket: the default access +/// method and VBoxVMI without a socket path fail, rather than guess a path. +#[cfg(all(feature = "virtualbox-vmi", not(feature = "virtualbox-api")))] #[test] -fn virtualbox_access_method_is_required() { - use microvmi::api::params::DriverInitParams; +fn virtualbox_default_socket_needs_virtualbox_api() { + use microvmi::api::params::{CommonInitParams, DriverInitParams, VBoxInitParams}; use microvmi::api::DriverType; - assert_eq!( - init_error(Some(DriverType::VirtualBox), DriverInitParams::default()), - "VirtualBox requires an explicit access method" - ); + for virtualbox in [None, Some(VBoxInitParams::VBoxVMI { socket_path: None })] { + let params = DriverInitParams { + common: Some(CommonInitParams { + vm_name: String::from("vm"), + }), + virtualbox, + ..Default::default() + }; + assert!(init_error(Some(DriverType::VirtualBox), params) + .starts_with("VirtualBox VBoxVMI method needs a socket path")); + } } #[cfg(feature = "virtualbox-vmi")] @@ -91,7 +97,7 @@ mod vbox_vmi { fn vboxvmi(dir: &TempDir) -> DriverInitParams { DriverInitParams { virtualbox: Some(VBoxInitParams::VBoxVMI { - socket_path: dir.socket().to_str().unwrap().to_string(), + socket_path: Some(dir.socket().to_str().unwrap().to_string()), }), ..Default::default() } diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs index 5ec0018c..ad11c3a0 100644 --- a/utilities/src/lib.rs +++ b/utilities/src/lib.rs @@ -72,7 +72,7 @@ impl Clappable for DriverInitParams { let virtualbox = matches .value_of("vbox_vmi") .map(|path| VBoxInitParams::VBoxVMI { - socket_path: String::from(path), + socket_path: Some(String::from(path)), }) .or_else(|| { matches @@ -131,7 +131,7 @@ mod tests { let params = DriverInitParams::from_matches(&matches); assert_eq!( VBoxInitParams::VBoxVMI { - socket_path: String::from("/tmp/vm.sock") + socket_path: Some(String::from("/tmp/vm.sock")) }, params.virtualbox.unwrap() );