From 263d145b49fa025ebe7812284ed65d15c52ad630 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 21:49:45 -0800 Subject: [PATCH 1/4] examples: connect to VMs through the public transport host-workflows' capture and restore asked their guest for a snapshot and activated a restore through ControlSession, the host's session of NVXC, NVX's own control protocol, which they reached at its public path, nvx::host::session, with session::connect. external-launch also connected to the boot console of its VM with session::connect. The examples say that a caller speaks its own protocol with its guest, and SL6.12 (#1127) makes NVXC and the host's session private to nvx: a caller of the library connects through the transport that nvx gives every caller. host-workflows' capture and restore now connect with transport::connect_verified, which checks that the VM's own process serves the endpoint, and attach with HostConnection::attach, the host's role of the NVXS session. The callbacks take that connection, on which the caller sends and receives its own protocol's messages, and fail with nvx::Error. failed() classifies a connection's failure as the restore expects it, as it did the session's: a VM that exited; a lost connection, whose broker closed or reset the session, whose stream ended inside a record, or whose transport broke; an expired deadline; an endpoint that another process serves; a malformed exchange; or I/O. external-launch connects to its boot console with transport::connect_verified and the VM process's reference, which session::connect built from the same process ID and start time and the host's boot. Evidence: cargo clippy --all-targets --all-features -- -D warnings compiles both examples on Windows. Part of #1127 (SL6.12). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/examples/external-launch.rs | 9 +- nvx-rs/examples/host-workflows.rs | 132 ++++++++++++++++------------- 2 files changed, 75 insertions(+), 66 deletions(-) diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index c168e53ff..ee8bd1ef5 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -23,7 +23,7 @@ use nvx::host::network::{Access, NetworkPolicy}; use nvx::host::platform::Transport; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; use nvx::host::supervisor::{self, Inherited}; -use nvx::host::{artifacts, report, session, snapshot, vm}; +use nvx::host::{artifacts, report, snapshot, transport, vm}; use nvx::protocol::root::RootImage; /// One byte past OpenVMM's 25-byte restore-ready event, so that a longer event fails to parse. @@ -180,12 +180,7 @@ fn main() -> Result<(), Box> { let console = outputs.console.as_deref().ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "no prepared boot console") })?; - let mut console = session::connect( - console, - process.reference().pid, - process.reference().start_time, - deadline, - )?; + let mut console = transport::connect_verified(console, process.reference(), deadline)?; read_until(console.as_mut(), b"NVX-GUEST-BOOT-OK:", deadline)?; console.write_all( b"printf 'NVX-EXTERNAL-LAUNCH-OK\\n'; /sbin/nvx-exit 0\n", diff --git a/nvx-rs/examples/host-workflows.rs b/nvx-rs/examples/host-workflows.rs index 8aedcf940..abe44aefc 100644 --- a/nvx-rs/examples/host-workflows.rs +++ b/nvx-rs/examples/host-workflows.rs @@ -26,9 +26,10 @@ use nvx::host::restore::{ ReadyListener, RestoreAction, RestoreEvidence, RestoreInput, RestoreMachine, RestorePhase, }; use nvx::host::runtime::Runtime; -use nvx::host::session::{self, ControlSession, SessionError}; use nvx::host::snapshot::{self, Snapshot}; +use nvx::host::transport::{self, HostConnection, HostConnectionError}; use nvx::host::vm::{self, Launched, Prepared, Reattached, SavedVm, Stopped, VmDir, VmOutputs}; +use nvx::protocol::nvxs::SessionError; /// One byte past OpenVMM's 25-byte restore-ready event, so that a longer event fails to parse. const READY_LIMIT: usize = 26; @@ -36,7 +37,7 @@ const READY_LIMIT: usize = 26; /// How long a failed capture or restore waits for the exit of the VM that it ends. const STOP_TIMEOUT: Duration = Duration::from_secs(10); -/// How long a lost session's VM has to show its exit. +/// How long a lost connection's VM has to show its exit. const EXIT_PAUSE: Duration = Duration::from_millis(50); fn main() {} @@ -105,13 +106,13 @@ pub fn cold_boot( /// directory, moves it to `snapshot_dir`, another, which must be empty, and opens it there. /// /// `request` asks the guest for a snapshot, in the caller's own protocol with its guest, on a -/// control session that only the VM's own process may serve, and returns once the guest answered. -/// The guest then captures the VM, as the guest example shows, and the VM exits once the capture -/// commits. The caller finishes the capture with the proof of that exit, then moves the -/// snapshot's three entries, and nothing else, out of the VM directory. It keeps them private -/// and immutable while it restores from them. When the request fails, or the VM doesn't exit by -/// `deadline`, the caller finishes no snapshot: it ends the VM, or keeps it in an [`Unstopped`] -/// error. +/// connection to the VM's control endpoint that only the VM's own process may serve, and returns +/// once the guest answered. The guest then captures the VM, as the guest example shows, and the +/// VM exits once the capture commits. The caller finishes the capture with the proof of that +/// exit, then moves the snapshot's three entries, and nothing else, out of the VM directory. It +/// keeps them private and immutable while it restores from them. When the request fails, or the +/// VM doesn't exit by `deadline`, the caller finishes no snapshot: it ends the VM, or keeps it in +/// an [`Unstopped`] error. pub fn capture( runtime: &Runtime, config: &VmConfig, @@ -121,7 +122,7 @@ pub fn capture( deadline: Instant, ) -> Result> where - R: FnOnce(&mut ControlSession, Instant) -> Result<(), SessionError>, + R: FnOnce(&mut HostConnection, Instant) -> nvx::Result<()>, { vm::create_private_dir(snapshot_dir)?; if fs::read_dir(snapshot_dir)?.next().is_some() { @@ -136,15 +137,13 @@ where .cloned() .ok_or("the configuration's boot has no capture")?; let launched = start(prepared)?; - let process = launched.process.reference(); - let requested = session::connect( + let requested = transport::connect_verified( &launched.outputs.endpoint, - process.pid, - process.start_time, + launched.process.reference(), deadline, ) - .and_then(|stream| ControlSession::attach(stream, launched.capability.as_bytes(), deadline)) - .and_then(|mut session| request(&mut session, deadline)); + .and_then(|stream| HostConnection::attach(stream, launched.capability.as_bytes(), deadline)) + .and_then(|mut connection| request(&mut connection, deadline)); if let Err(error) = requested { return Err(end(launched, error.into())); } @@ -168,7 +167,7 @@ where } /// Restores `snapshot` with `config`, which has no `boot`, in `directory`, a new private -/// directory, and returns the running VM and its control session once its guest is Ready. +/// directory, and returns the running VM and its connection to the guest once the guest is Ready. /// /// Creation hard-links the snapshot's OpenVMM files into the VM directory, a view that the /// caller removes once the VM exited. On Linux, the caller holds its own lock of the snapshot, @@ -188,9 +187,9 @@ pub fn restore( transaction: TransactionId, activate: A, deadline: Instant, -) -> Result<(Launched, ControlSession), Box> +) -> Result<(Launched, HostConnection), Box> where - A: FnMut(&mut ControlSession, &[u8], Instant) -> Result, + A: FnMut(&mut HostConnection, &[u8], Instant) -> nvx::Result, { vm::create_private_dir(directory)?; let dir = VmDir::open(directory)?; @@ -198,25 +197,25 @@ where let record = prepared.activation_record(transaction)?; let mut launched = start(prepared)?; match drive(&mut launched, &record, activate, deadline) { - Ok(session) => Ok((launched, session)), + Ok(connection) => Ok((launched, connection)), Err(error) => Err(end(launched, error)), } } -/// Drives a launched restore to its guest's Ready, and returns the session in which it came. +/// Drives a launched restore to its guest's Ready, and returns the connection in which it came. /// /// The restore's machine says what to do next and checks what the caller observed: the -/// restore-ready event, which only the VM's own process may send, a control session, which only -/// it may serve, and the guest's Ready, which must echo the record's transaction. A lost session -/// is replaced, with the same record, until the deadline. +/// restore-ready event, which only the VM's own process may send, a connection to the VM's +/// control endpoint, which only it may serve, and the guest's Ready, which must echo the record's +/// transaction. A lost connection is replaced, with the same record, until the deadline. fn drive( launched: &mut Launched, record: &ActivationRecord, mut activate: A, deadline: Instant, -) -> Result> +) -> Result> where - A: FnMut(&mut ControlSession, &[u8], Instant) -> Result, + A: FnMut(&mut HostConnection, &[u8], Instant) -> nvx::Result, { let mut machine = RestoreMachine::new(record); machine.input(RestoreInput::Spawned(launched.process.reference().clone())); @@ -224,7 +223,7 @@ where while let Some(action) = machine.next_action() { let input = match action { RestoreAction::Done => { - return current.ok_or_else(|| "a restore that is Ready has its session".into()); + return current.ok_or_else(|| "a restore that is Ready has its connection".into()); } RestoreAction::Fail(error) => return Err(error.into()), _ if Instant::now() >= deadline => RestoreInput::Expired, @@ -251,26 +250,26 @@ where } } RestoreAction::Connect => { - // A session that was lost closes before another replaces it. + // A connection that was lost closes before another replaces it. current = None; let process = launched.process.reference(); let endpoint = &launched.outputs.endpoint; let capability = launched.capability.as_bytes(); - match session::connect(endpoint, process.pid, process.start_time, deadline) - .and_then(|stream| ControlSession::attach(stream, capability, deadline)) + match transport::connect_verified(endpoint, process, deadline) + .and_then(|stream| HostConnection::attach(stream, capability, deadline)) { - Ok(session) => { - current = Some(session); + Ok(connection) => { + current = Some(connection); RestoreInput::Connected } Err(error) => failed(launched, RestorePhase::Connect, error), } } RestoreAction::SendActivate(bytes) => { - let session = current + let connection = current .as_mut() - .ok_or("a restore activates in its session")?; - match activate(session, &bytes, deadline) { + .ok_or("a restore activates in its connection")?; + match activate(connection, &bytes, deadline) { Ok(ready) => RestoreInput::GuestReady(ready), Err(error) => failed(launched, RestorePhase::Activate, error), } @@ -301,47 +300,62 @@ fn ready_event( Ok(Some(event)) } -/// What a control session's failure in `phase` tells the restore: the VM's exit, a lost session, -/// which another may replace, an expired deadline, or a final failure. +/// What a failure of the connection to the guest in `phase` tells the restore: the VM's exit, a +/// lost connection, which another may replace, an expired deadline, or a final failure. /// -/// A VM that exits ends its sessions before its exit shows, so a lost session's VM has a moment -/// to show its exit first. -fn failed(launched: &Launched, phase: RestorePhase, error: SessionError) -> RestoreInput { - let lost = match &error { - SessionError::Closed | SessionError::Reset => true, - SessionError::Io(error) => matches!( - error.kind(), - io::ErrorKind::BrokenPipe - | io::ErrorKind::ConnectionAborted - | io::ErrorKind::ConnectionReset - | io::ErrorKind::UnexpectedEof - | io::ErrorKind::WriteZero - ), - _ => false, - }; +/// A VM that exits ends its connections before its exit shows, so a lost connection's VM has a +/// moment to show its exit first. +fn failed(launched: &Launched, phase: RestorePhase, error: nvx::Error) -> RestoreInput { + // The broker closed or reset the session, or the stream ended inside a record. + let closed = error.source().is_some_and(|cause| { + matches!( + cause.downcast_ref(), + Some(HostConnectionError::Closed | HostConnectionError::Reset(_)) + ) || matches!(cause.downcast_ref(), Some(SessionError::Truncated { .. })) + }); + let lost = closed + || io_cause(&error).is_some_and(|cause| { + matches!( + cause.kind(), + io::ErrorKind::BrokenPipe + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::ConnectionReset + | io::ErrorKind::UnexpectedEof + | io::ErrorKind::WriteZero + ) + }); let gone = lost && launched .process .wait(Instant::now() + EXIT_PAUSE) .is_ok_and(|exit| exit.is_some()); - let evidence = match error { - SessionError::ProcessExited => return exited(&launched.outputs, phase), + let evidence = match error.kind() { + ErrorKind::Exited => return exited(&launched.outputs, phase), _ if gone => return exited(&launched.outputs, phase), _ if lost => return RestoreInput::SessionLost, - SessionError::TimedOut => return RestoreInput::Expired, - SessionError::Refused(_) => RestoreEvidence::PeerMismatch, - SessionError::Protocol(_) | SessionError::AuthenticationFailed => { - RestoreEvidence::Malformed + ErrorKind::TimedOut => return RestoreInput::Expired, + // `transport::connect_verified` refuses an endpoint that another process serves. + ErrorKind::Protocol + if io_cause(&error) + .is_some_and(|cause| cause.kind() == io::ErrorKind::PermissionDenied) => + { + RestoreEvidence::PeerMismatch } + ErrorKind::Protocol => RestoreEvidence::Malformed, _ => RestoreEvidence::Io, }; RestoreInput::OperationFailed { phase, evidence, - source: Some(Box::new(error)), + source: Some(error.into()), } } +/// The I/O error that caused `error`, if one did. +fn io_cause(error: &nvx::Error) -> Option<&io::Error> { + error.source()?.downcast_ref() +} + /// The VM's exit, with the outcome report that tells what the failure is attributed to. fn exited(outputs: &VmOutputs, phase: RestorePhase) -> RestoreInput { match report::read(&outputs.outcome) { From 78f404340b8542d6f494856933ebce0f93dce0cc Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 21:50:48 -0800 Subject: [PATCH 2/4] tests: encode the VM lifecycle's PING and STOP frames in the test tests/vm_lifecycle.rs drives a cold VM through the public API alone, and asks its guest, nvx-init, whether it is ready and to end the VM in the guest's protocol, NVXC, whose frames it built and read with nvx::protocol::{encode_app, decode_app} and the APP_* kinds. SL6.12 (#1127) makes NVXC private to nvx, as NVX's own protocol, so a caller of the public API that runs nvx-init speaks NVXC on its own, over the HostConnection that vm::reattach returns, as this test's caller does. The test now encodes the empty NVXC frames that it sends, PING and STOP, and the acknowledgements that it expects, READY and STOPPED, itself: the magic, the version, the kind, zero flags, the request's ID, a zero status, and a zero length, as tests/vectors/nvxc-v1.txt pins them. It compares each answer with the whole acknowledgement frame, which checks the magic, version, flags, kind, request ID, status and empty payload that decode_app and its assertion checked. It stays a test of the public API, which the case matrix's vm-lifecycle row names, so the scenario that runs it is unchanged. Evidence: cargo clippy --test vm_lifecycle -- -D warnings passes on Windows; the frame matches the NVXC PING frame of the DATA_PING record in tests/vectors/nvxc-v1.txt. Part of #1127 (SL6.12). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/tests/vm_lifecycle.rs | 57 ++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/nvx-rs/tests/vm_lifecycle.rs b/nvx-rs/tests/vm_lifecycle.rs index 109c7c54a..2865a123c 100644 --- a/nvx-rs/tests/vm_lifecycle.rs +++ b/nvx-rs/tests/vm_lifecycle.rs @@ -29,13 +29,21 @@ use nvx::host::runtime::{ use nvx::host::transport::HostConnection; use nvx::host::vm::{self, SavedVm, VmDir, VmOutputs}; use nvx::protocol::root::RootImage; -use nvx::protocol::{self, APP_PING, APP_READY, APP_STOP, APP_STOPPED}; use nvx::uuid::Uuid; /// The caller's kernel command-line tokens, which `nvx-rs sandbox start` gives nvx-init too: it /// manages the VM, and runs workloads as the fixed identity `65534:65534`. const COMMAND_LINE: &str = "nvx_sandbox=1 nvx_mode=managed nvx_identity=65534:65534"; +/// The request of NVXC, nvx-init's protocol, that asks the guest whether it is ready. +const PING: u8 = 1; +/// The request of NVXC that asks the guest to end the VM. +const STOP: u8 = 3; +/// NVXC's acknowledgement of [`PING`]. +const READY: u8 = 0x81; +/// NVXC's acknowledgement of [`STOP`]. +const STOPPED: u8 = 0x85; + /// The inputs of the test, which the scenario gives it as a JSON object in `NVX_VM_LIFECYCLE`. #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] @@ -92,8 +100,28 @@ impl Drop for Started { } } -/// Sends the request `kind` of the caller's own protocol with its guest, which for nvx-init is -/// NVXC, and checks that the guest acknowledges it with `reply` by `deadline`. +/// The NVXC frame of `kind` for the request `request`, with status 0 and no payload: the magic, +/// the version, 1, the kind, two zero flag bytes, the request's ID, the status, and the payload's +/// length, each integer little-endian, as `tests/vectors/nvxc-v1.txt` pins them. +fn frame(kind: u8, request: u64) -> Vec { + [ + &b"NVXC"[..], + &[1, kind, 0, 0], + &request.to_le_bytes(), + &0i32.to_le_bytes(), + &0u32.to_le_bytes(), + ] + .concat() +} + +/// Sends the request `kind` of the caller's own protocol with its guest, and checks that the guest +/// acknowledges it with `reply` by `deadline`. +/// +/// `nvx` carries the bytes of whatever protocol a caller speaks with its guest over the +/// `HostConnection` that `vm::reattach` returns, and leaves the protocol to the caller. This +/// caller's guest is nvx-init, which speaks NVX's own protocol, NVXC; NVXC is private to `nvx`, +/// so the caller encodes the few frames that it sends and expects itself, as any caller of a +/// guest of its own would. fn acknowledged( connection: &mut HostConnection, kind: u8, @@ -102,15 +130,14 @@ fn acknowledged( deadline: Instant, ) { connection - .send(&protocol::encode_app(kind, request, 0, &[]), Some(deadline)) + .send(&frame(kind, request), Some(deadline)) .expect("the guest takes the request"); let response = connection .receive(Some(deadline)) .expect("the guest answers the request"); - let frame = protocol::decode_app(&response).expect("the answer is an NVXC frame"); assert_eq!( - (frame.kind, frame.request_id, frame.status, frame.payload), - (reply, request, 0, &[][..]), + response, + frame(reply, request), "the guest acknowledges request {kind}" ); } @@ -203,9 +230,9 @@ fn a_cold_vm_is_reattached_from_saved_values_and_stopped() { assert_eq!(reattached.process.reference(), &saved.process); acknowledged( &mut reattached.connection, - APP_PING, + PING, 1, - APP_READY, + READY, Instant::now() + timeout, ); drop(reattached); @@ -213,9 +240,9 @@ fn a_cold_vm_is_reattached_from_saved_values_and_stopped() { .expect("the saved values reattach the VM again, with a new connection"); acknowledged( &mut reattached.connection, - APP_PING, + PING, 2, - APP_READY, + READY, Instant::now() + timeout, ); let grace = Instant::now() + timeout; @@ -223,13 +250,7 @@ fn a_cold_vm_is_reattached_from_saved_values_and_stopped() { &reattached.process, &started.outputs, |deadline| { - acknowledged( - &mut reattached.connection, - APP_STOP, - 3, - APP_STOPPED, - deadline, - ); + acknowledged(&mut reattached.connection, STOP, 3, STOPPED, deadline); true }, grace, From 6db32ac66beabe1ee53b105eb2249d395d61c862 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 22:06:02 -0800 Subject: [PATCH 3/4] host: make the control session private to the CLI nvx::host::session, the host's session of NVXC, NVX's own control protocol, was public API of nvx, though only NVX's programs use it: the CLI, with the sessions of nvx-rs sandbox and its snapshot capture, and the restore driver, which the cli feature builds. SL6.12 (#1127) makes it private where it is, before NVXC v2 changes it. The module becomes crate-private and cli-gated, and its items pub(crate). The CLI's own session sends PING, FEATURES, EXEC and STOP through send_request and read_response, so nothing in NVX calls the session's ping, features, start_exec, cancel_exec or stop any more. They keep cfg_attr(not(test), expect(dead_code)) hooks, which name SL4.4c, whose sandbox exec and run ping, exec, cancel and stop through the session, and, for features, SL4.4d, which deletes FEATURES; the allowlist's seams of session.rs go from 1 to 5. capture's hook and activate's cfg go, since the module is the CLI's alone, and APP_CAPTURE and encode_capture_payload, which only the session's capture sends, follow it from host to cli. tests/managed_lifecycle.rs, which drove the managed lifecycle of a real guest through nvx::host::session, moves with git mv to the session's tests, as host::session::tests::managed_lifecycle, unchanged but for its imports, which use crate paths, and its cfg, which its module declaration holds. The managed-lifecycle scenario runs it with `cargo test --features cli --lib -- --ignored --exact` and its path. The tests that reach the session from outside it follow its gate: two transport tests, which compare connect_verified with the session's legacy connect, with their exchange helper; the live restored-init tests of host::restore; and the managed agent's loopback test, whose host is the session. The public-API snapshots drop host::session's items, and only those. The README and the docs describe the session as private, and the managed-lifecycle test where it now lives. Evidence, on Windows with Rust 1.95.0: cargo clippy -D warnings passes for --all-targets --all-features, --lib, --lib --no-default-features, serde alone, --lib --bins with cli, and with testing alone, and for x86_64-unknown-linux-gnu --all-targets --all-features and x86_64-unknown-linux-musl with guest alone and --all-targets; cargo doc -D warnings, public_api.py and check_test_layout.py pass; cargo test --all-features --lib passes for host::session, host::transport, host::restore, host::cli::session, host::cli::snapshots and host::restore_driver (110 passed, 4 ignored), and cargo test --lib passes for host::transport and host::restore with the default features; python -m unittest test_microvm_tests -k managed_lifecycle passes. Part of #1127 (SL6.12). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/acceptance-case-matrix.md | 2 +- doc/adversarial-testing.md | 5 +- doc/ci.md | 5 +- doc/contribute.md | 3 +- doc/project-structure.md | 2 +- doc/usage.md | 5 +- nvx-rs/README.md | 50 +++++++------ nvx-rs/public-api/linux.txt | 36 ---------- nvx-rs/public-api/windows.txt | 36 ---------- nvx-rs/src/guest/managed_agent/tests.rs | 2 +- nvx-rs/src/host/mod.rs | 16 +++-- nvx-rs/src/host/restore/tests.rs | 1 + nvx-rs/src/host/session.rs | 72 ++++++++++++++----- nvx-rs/src/host/session/tests.rs | 3 + .../host/session}/tests/managed_lifecycle.rs | 9 ++- nvx-rs/src/host/transport/tests.rs | 4 ++ nvx-rs/src/lib.rs | 4 +- nvx-rs/src/protocol/app.rs | 14 ++-- nvx-rs/src/protocol/mod.rs | 4 +- scripts/nvx_tools/microvm_tests.py | 28 ++++++-- scripts/nvx_tools/sandbox_lifecycle_tests.py | 7 +- scripts/test-layout-allowlist.json | 2 +- scripts/test_microvm_tests.py | 21 ++++-- 23 files changed, 172 insertions(+), 159 deletions(-) rename nvx-rs/{ => src/host/session}/tests/managed_lifecycle.rs (99%) diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index 77ce7a011..422701095 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -324,7 +324,7 @@ suite in its shards, and the public sandbox scenarios in its guest and sandbox j | `l3-l4-egress-protocols` | kept | | | `lifecycle` | kept | | | `managed-exec-config` | kept | A [cold cell](#cold-boots): the EROFS layer over a scratch upper, through `provision` and `start` | -| `managed-lifecycle` | kept | | +| `managed-lifecycle` | kept | It runs the ignored unit test of the host's private control session in `nvx-rs/src/host/session/tests/managed_lifecycle.rs`, with the `cli` feature, which SL6.12 (#1127) moved there from the crate's integration tests | | `network-snapshot` | kept | | | `platform-aggregate-export` | kept | | | `platform-filesystem` | kept | | diff --git a/doc/adversarial-testing.md b/doc/adversarial-testing.md index 4ab831faa..52ebadb86 100644 --- a/doc/adversarial-testing.md +++ b/doc/adversarial-testing.md @@ -157,7 +157,8 @@ metadata and summary record the absolute target artifact root. Besides the guest and OpenVMM artifacts, a target needs nvx-rs, the Rust command-line interface that runs NVX's sandboxes and microVMs ([Usage](usage.md#nvx-rs)). Of the catalogued scenarios, `managed-lifecycle` -runs the nvx-rs test `managed_lifecycle` with Cargo, and +runs the nvx-rs unit test of its host control session, +`host::session::tests::managed_lifecycle`, with Cargo, and `l3-l4-egress-policy` compiles its egress policy with nvx-rs and runs `nvx-rs run`. They find nvx-rs at `NVX_RS`, then at `nvx-rs/target/release/nvx-rs[.exe]`, and otherwise they build it there with @@ -177,7 +178,7 @@ of the nvx-rs build: ```bash cargo build --release --locked --features cli --manifest-path nvx-rs/Cargo.toml --target-dir nvx-rs/target -cargo test --no-run --locked --manifest-path nvx-rs/Cargo.toml --test managed_lifecycle +cargo test --no-run --locked --manifest-path nvx-rs/Cargo.toml --features cli --lib python3 scripts/nvx.py record-nvx-rs-provenance ``` diff --git a/doc/ci.md b/doc/ci.md index e044ba784..ebe75e301 100644 --- a/doc/ci.md +++ b/doc/ci.md @@ -697,8 +697,9 @@ both through the [`package-release`](../.github/actions/package-release/action.yml) action, the provenance into `build/`, so that each package ships the `nvx-rs` of its operating system. The `managed-lifecycle` and `vm-lifecycle` scenarios also -build and run the ignored `nvx-rs` tests `tests/managed_lifecycle.rs` and -`tests/vm_lifecycle.rs` with Cargo on the runner. +build and run the ignored `nvx-rs` tests +`src/host/session/tests/managed_lifecycle.rs`, a unit test of the library with +the `cli` feature, and `tests/vm_lifecycle.rs` with Cargo on the runner. Once the required artifacts are ready, benchmarks run in parallel with the NVX test layer, each on its backend's diff --git a/doc/contribute.md b/doc/contribute.md index 45ea0e118..12e41c47b 100644 --- a/doc/contribute.md +++ b/doc/contribute.md @@ -136,7 +136,8 @@ registers them. `sandbox` and the control protocol belong to `nvx-rs` scenarios drive them as commands, compile egress policies with `nvx-rs egress compile`, and leave the control session to `nvx-rs`: the `managed-lifecycle` scenario builds OpenVMM's command line and runs the ignored -`nvx-rs` test `tests/managed_lifecycle.rs`, which drives the guest, and the +`nvx-rs` unit test `src/host/session/tests/managed_lifecycle.rs`, which drives +the guest, and the `vm-lifecycle` scenario runs the ignored test `tests/vm_lifecycle.rs`, which drives a VM through the public API of `nvx`. Add no Python client of the control protocol, lifecycle, or egress logic. diff --git a/doc/project-structure.md b/doc/project-structure.md index 14457a8b5..fc726a13e 100644 --- a/doc/project-structure.md +++ b/doc/project-structure.md @@ -107,7 +107,7 @@ nvx/ | | |-- guest/ Programs that run in Linux guests, the guest's NVXS connection, the libraries through which any guest init or agent opens the terminal on which it serves its session, starts a program, collects its children's statuses, settles its executions' cgroups, drives each execution, mounts host mappings beneath the descriptors of the roots that it names, assembles and tears down a layered root, and reads a GPT disk's partition table through its block device, and the typed calls through which it runs the time ABI's steps and the reseed (feature `guest`) | | `-- bin/ nvx-rs CLI, nvx-fake-openvmm test double, and the guest binaries | |-- examples/ The external-launch example, and the documented host and guest workflows, which only compile -| |-- tests/ Fake-OpenVMM behavior, process and VM spawn, CLI scenario, NVXS session, guest-binary, and hypervisor managed-lifecycle and public-API VM lifecycle tests, the security checklist, sandbox configurations of every format, the cases of the export and root boot tokens, and the conformance vectors of the wire formats +| |-- tests/ Fake-OpenVMM behavior, process and VM spawn, CLI scenario, NVXS session, guest-binary, and hypervisor public-API VM lifecycle tests, the security checklist, sandbox configurations of every format, the cases of the export and root boot tokens, and the conformance vectors of the wire formats | |-- public-api/ The crate's public API on Linux and on Windows, which CI checks | |-- build.rs Stages the bundled artifacts (feature `bundled`) | `-- artifacts.json Release package pinned for the bundled artifacts diff --git a/doc/usage.md b/doc/usage.md index 74c92040b..38fe88dcc 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -1303,8 +1303,9 @@ the hooked rate of host UTC, then restores a snapshot with another rate, and with one beyond Linux's 500 ppm limit, which the guest must hold at the limit and report. The `managed-lifecycle` scenario boots a guest whose init hands -the boot to the managed agent, and runs the ignored `nvx-rs` test -`tests/managed_lifecycle.rs` with Cargo, which starts OpenVMM with the +the boot to the managed agent, and runs the ignored `nvx-rs` unit test of the +host's control session, in `src/host/session/tests/managed_lifecycle.rs`, with +Cargo, which starts OpenVMM with the scenario's command line and drives the guest's control session; the scenario then checks OpenVMM's outcome report and the guest console. The `vm-lifecycle` scenario runs the ignored `nvx-rs` test diff --git a/nvx-rs/README.md b/nvx-rs/README.md index bb158b53b..92625b9e6 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -131,7 +131,7 @@ changes only when the module's purpose does. | [`nvx::host::process`](#nvxhostprocess) | Verified references to OpenVMM processes, through which callers wait for them and end them | | [`nvx::host::supervisor`](#nvxhostsupervisor) | The transitional public path of the low-level spawn of a VMM, which code that stays uses until SL6.3 (#1122) | | [`nvx::host::seal`](#nvxhostseal) | `FileSeal`: what the host reports about an open file from its metadata alone, without reading it | -| [`nvx::host::session`, `platform`, `report`](#nvxhostsession-platform-report) | The authenticated control session, Linux and Windows primitives, and OpenVMM's outcome report | +| [`nvx::host::platform`, `report`](#nvxhostplatform-report) | Linux and Windows primitives, and OpenVMM's outcome report | | [`nvx::host::sandbox`](#nvxhostsandbox) | The launch profile of `nvx-rs sandbox`: its plans, live shares, option checks, bindings, and prepared launches | | [`nvx::guest`](#nvxguest) | The programs that run in Linux guests; see [Guest binaries](#guest-binaries) | | [`nvx::guest::mappings`](#nvxguestmappings) | `Mapping` and `MappingTable`: host mappings, and the account of a host's mapping table, for any guest init or agent | @@ -1054,7 +1054,8 @@ and refuses unverifiable peers rather than falling back to a numeric PID. An indeterminate process check remains an error, never exit proof. `HostConnection`: the host's role of the NVXS session over a platform transport, -on which `nvx::host::session` and other host applications run their protocols. +on which NVX's own control session and other host applications run their +protocols. It attaches with the launch capability, waiting while the broker asks it to, sends application bytes in as many DATA records as they take, and returns each DATA payload from the guest, with deadlines; a receive that times out keeps a @@ -1069,8 +1070,8 @@ remain available as sources. `Transport` and `TransportHandle` re-export the existing platform types, with one definition beneath the OS adapters. Timed reads and writes, native-handle duplication and `unread` are unchanged: Linux reports Unix `SIOCOUTQ` charges, -and Windows reports `None`. The old `host::session::connect` remains a -compatibility wrapper. +and Windows reports `None`. The old `session::connect`, private to the crate +like the session, remains a compatibility wrapper for the CLI. ### `nvx::host::process` @@ -1178,11 +1179,12 @@ file ID, the kind, the length, the modification time, and on Linux the change time, with a versioned serde form. A seal detects accidental change, not tampering; see [Distro bindings](#distro-bindings). -### `nvx::host::session`, `platform`, `report` +### `nvx::host::platform`, `report` -The authenticated control session, which carries NVX's control protocol, `NVXC`, -one frame per DATA record of a `HostConnection`; Linux and Windows primitives; -and OpenVMM's outcome report. +Linux and Windows primitives, and OpenVMM's outcome report. The authenticated +control session, which carries NVX's own control protocol, `NVXC`, one frame +per DATA record of a `HostConnection`, is private to the crate: the CLI and its +restore driver use it, with the `cli` feature. `report::read` returns `Ok(None)` for a missing report, and an `Io` error for an unreadable or malformed report, retaining the underlying error. It keeps today's @@ -2640,9 +2642,9 @@ guest agents of NVX advertise bits 0, 4, and 6 in sandbox mode. treats as one that has not appeared yet, and, on Windows, starts OpenVMM without a console window, as `nvx::host::vm::spawn` starts it. - `start` stops waiting for the control endpoint once the OpenVMM that it - launched exits, as `nvx::host::session::connect` does, and fails at once - with `exited`, where `nvx.py` waited until `--timeout` passed before it - reported the exit. + launched exits, as `nvx::host::transport::connect_verified` does, and fails + at once with `exited`, where `nvx.py` waited until `--timeout` passed before + it reported the exit. - `exec` refuses more than 1 MiB of workload output, which the guest agent never forwards. - `provision`, and `start` again, refuse a share whose resolved host path @@ -3105,10 +3107,11 @@ simulated workload, transport, and sink on a clock of their own, and, on Linux, the tests of `nvx::guest::exec::binding` drive real programs through them, two at once in one loop, one of them with a stalled sink; run as root, one of them also settles a real cgroup. -On Linux, a loopback test drives -the host `ControlSession` through a broker shim into the real managed agent's -session, dispatch, and execution engine, and `nvx.py` also lints and tests the -guest binaries, and runs their integration tests against a build of them: +On Linux, with the `cli` feature, a loopback test drives +the host's private `ControlSession` through a broker shim into the real managed +agent's session, dispatch, and execution engine, and `nvx.py` also lints and +tests the guest binaries, and runs their integration tests against a build of +them: ```bash python3 ../scripts/nvx.py check-guest-binaries @@ -3202,14 +3205,15 @@ the submodule out, and so does an OpenVMM pin promotion; see profiles from which [`run`](#run) guides a failed cold boot, with the profiles that the submodule pins, and runs where that one does. -`tests/managed_lifecycle.rs` is an ignored test of the managed lifecycle of a -guest whose init hands the boot to the managed agent, on a real hypervisor: -authenticated sessions and reconnects, request-scoped working directories and -environments, the workload identity, timeouts, refused working directories, -and stop, which `nvx::host::session` drives. `nvx.py test-microvm --scenario -managed-lifecycle` builds OpenVMM's command line for its guest and backend, -runs the test with Cargo, and checks the outcome report and the guest console -that it leaves: +`src/host/session/tests/managed_lifecycle.rs` is an ignored unit test of the +host's private control session, which the `cli` feature builds: the managed +lifecycle of a guest whose init hands the boot to the managed agent, on a real +hypervisor: authenticated sessions and reconnects, request-scoped working +directories and environments, the workload identity, timeouts, refused working +directories, and stop, which the session drives. `nvx.py test-microvm +--scenario managed-lifecycle` builds OpenVMM's command line for its guest and +backend, runs the test with Cargo, and checks the outcome report and the guest +console that it leaves: ```bash python3 ../scripts/nvx.py test-microvm --backend kvm --scenario managed-lifecycle diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 408037061..cce400036 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -2894,42 +2894,6 @@ variant nvx::host::seal::SealScheme::Windows struct nvx::host::seal::SealTime field nvx::host::seal::SealTime::nanoseconds field nvx::host::seal::SealTime::seconds -mod nvx::host::session -struct nvx::host::session::ControlSession -method nvx::host::session::ControlSession::attach -method nvx::host::session::ControlSession::cancel_exec -method nvx::host::session::ControlSession::features -method nvx::host::session::ControlSession::map_host_paths -method nvx::host::session::ControlSession::next_exec_event -method nvx::host::session::ControlSession::ping -method nvx::host::session::ControlSession::read_response -method nvx::host::session::ControlSession::send_request -method nvx::host::session::ControlSession::start_exec -method nvx::host::session::ControlSession::stop -enum nvx::host::session::ExecEvent -variant nvx::host::session::ExecEvent::Exit -field nvx::host::session::ExecEvent::Exit::category -field nvx::host::session::ExecEvent::Exit::status -variant nvx::host::session::ExecEvent::Rejected -field nvx::host::session::ExecEvent::Rejected::category -field nvx::host::session::ExecEvent::Rejected::status -variant nvx::host::session::ExecEvent::Stderr -field nvx::host::session::ExecEvent::Stderr::0 -variant nvx::host::session::ExecEvent::Stdout -field nvx::host::session::ExecEvent::Stdout::0 -enum nvx::host::session::SessionError -variant nvx::host::session::SessionError::AuthenticationFailed -variant nvx::host::session::SessionError::Closed -variant nvx::host::session::SessionError::Io -field nvx::host::session::SessionError::Io::0 -variant nvx::host::session::SessionError::ProcessExited -variant nvx::host::session::SessionError::Protocol -field nvx::host::session::SessionError::Protocol::0 -variant nvx::host::session::SessionError::Refused -field nvx::host::session::SessionError::Refused::0 -variant nvx::host::session::SessionError::Reset -variant nvx::host::session::SessionError::TimedOut -fn nvx::host::session::connect mod nvx::host::snapshot struct nvx::host::snapshot::BlockGeometry field nvx::host::snapshot::BlockGeometry::length diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index ae29c4eee..14a0154a3 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1497,42 +1497,6 @@ variant nvx::host::seal::SealScheme::Windows struct nvx::host::seal::SealTime field nvx::host::seal::SealTime::nanoseconds field nvx::host::seal::SealTime::seconds -mod nvx::host::session -struct nvx::host::session::ControlSession -method nvx::host::session::ControlSession::attach -method nvx::host::session::ControlSession::cancel_exec -method nvx::host::session::ControlSession::features -method nvx::host::session::ControlSession::map_host_paths -method nvx::host::session::ControlSession::next_exec_event -method nvx::host::session::ControlSession::ping -method nvx::host::session::ControlSession::read_response -method nvx::host::session::ControlSession::send_request -method nvx::host::session::ControlSession::start_exec -method nvx::host::session::ControlSession::stop -enum nvx::host::session::ExecEvent -variant nvx::host::session::ExecEvent::Exit -field nvx::host::session::ExecEvent::Exit::category -field nvx::host::session::ExecEvent::Exit::status -variant nvx::host::session::ExecEvent::Rejected -field nvx::host::session::ExecEvent::Rejected::category -field nvx::host::session::ExecEvent::Rejected::status -variant nvx::host::session::ExecEvent::Stderr -field nvx::host::session::ExecEvent::Stderr::0 -variant nvx::host::session::ExecEvent::Stdout -field nvx::host::session::ExecEvent::Stdout::0 -enum nvx::host::session::SessionError -variant nvx::host::session::SessionError::AuthenticationFailed -variant nvx::host::session::SessionError::Closed -variant nvx::host::session::SessionError::Io -field nvx::host::session::SessionError::Io::0 -variant nvx::host::session::SessionError::ProcessExited -variant nvx::host::session::SessionError::Protocol -field nvx::host::session::SessionError::Protocol::0 -variant nvx::host::session::SessionError::Refused -field nvx::host::session::SessionError::Refused::0 -variant nvx::host::session::SessionError::Reset -variant nvx::host::session::SessionError::TimedOut -fn nvx::host::session::connect mod nvx::host::snapshot struct nvx::host::snapshot::BlockGeometry field nvx::host::snapshot::BlockGeometry::length diff --git a/nvx-rs/src/guest/managed_agent/tests.rs b/nvx-rs/src/guest/managed_agent/tests.rs index c4da4b053..5f3f6f9bc 100644 --- a/nvx-rs/src/guest/managed_agent/tests.rs +++ b/nvx-rs/src/guest/managed_agent/tests.rs @@ -3,7 +3,7 @@ use super::*; use crate::guest::entry::AccountMode; pub(crate) use crate::guest::sys::tests::isolate_process_test; -#[cfg(feature = "host")] +#[cfg(feature = "cli")] /// In-process host/broker/agent integration harness. mod loopback; diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 2baff95b2..baca8e201 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -24,10 +24,11 @@ //! - [`snapshot`] holds the compatibility key, [`SnapshotKey`](snapshot::SnapshotKey), derived //! from a [`CaptureClass`](snapshot::CaptureClass) and [`SnapshotManifest`](snapshot::SnapshotManifest), //! the strict manifest reader, and the capture's public time and eligibility information. -//! - [`session`] speaks NVX's control protocol, `NVXC`, over [`transport`], which verifies a VM's -//! endpoint and runs the host's role of the NVXS session for it and for any other application -//! protocol, over a platform transport. [`nvxs`] retains compatibility paths. [`platform`] holds -//! the primitives of Linux and Windows hosts. Other hosts compile but report NVX as unavailable. +//! - [`transport`] verifies a VM's endpoint and runs the host's role of the NVXS session for it, +//! for any application protocol, over a platform transport. The private `session` (feature +//! `cli`) speaks NVX's own control protocol, `NVXC`, over it for NVX's programs. [`nvxs`] +//! retains compatibility paths. [`platform`] holds the primitives of Linux and Windows hosts. +//! Other hosts compile but report NVX as unavailable. //! - [`process`] names processes by verified references: their ID, start time, and host boot, //! through which a pidfd or a process handle holds them, so that waits sleep until a process //! exits and termination reaches only it, whether or not the caller started it. @@ -45,8 +46,8 @@ //! the VM directory and binds its restore-ready listener before any spawn. //! - [`restore`] orders caller-driven activation to Ready without I/O or a clock, and listens for //! OpenVMM's restore-ready event from the held VM process. The private `restore_driver` (feature -//! `cli`) drives it over the [`session`] for NVX's blocking callers, through Ready, the mapping -//! table and the workload's start. +//! `cli`) drives it over the private `session` for NVX's blocking callers, through Ready, the +//! mapping table and the workload's start. //! - `cli` (feature `cli`) is the `nvx-rs` command-line interface, which runs microVMs and //! sandboxes, looks up and downloads artifacts, and compiles egress policies. //! @@ -85,7 +86,8 @@ mod restore_driver; pub mod runtime; pub mod sandbox; pub mod seal; -pub mod session; +#[cfg(feature = "cli")] +pub(crate) mod session; pub mod snapshot; mod spawn; pub mod supervisor; diff --git a/nvx-rs/src/host/restore/tests.rs b/nvx-rs/src/host/restore/tests.rs index 918e04ae3..4c5a2bbb9 100644 --- a/nvx-rs/src/host/restore/tests.rs +++ b/nvx-rs/src/host/restore/tests.rs @@ -1,6 +1,7 @@ #![cfg(any(target_os = "linux", windows))] #![cfg_attr(windows, allow(unsafe_code))] +#[cfg(feature = "cli")] mod live_init; use std::io::{BufRead, BufReader, Write}; diff --git a/nvx-rs/src/host/session.rs b/nvx-rs/src/host/session.rs index 74b23fe57..ecbb429f0 100644 --- a/nvx-rs/src/host/session.rs +++ b/nvx-rs/src/host/session.rs @@ -18,7 +18,7 @@ use crate::{Error, ErrorKind}; /// Failure of a control session. #[derive(Debug)] -pub enum SessionError { +pub(crate) enum SessionError { /// The deadline passed. TimedOut, /// The OpenVMM process exited. @@ -74,7 +74,7 @@ impl From for SessionError { /// Connects to the control endpoint of OpenVMM process `pid`, retrying until `deadline` while /// the endpoint is not yet available and the process is still alive. -pub fn connect( +pub(crate) fn connect( endpoint: &str, pid: u32, start_time: u64, @@ -114,7 +114,7 @@ fn connect_failure(error: Error) -> SessionError { /// Event observed while an execution runs. #[derive(Debug, PartialEq, Eq)] -pub enum ExecEvent { +pub(crate) enum ExecEvent { /// A chunk of the workload's standard output. Stdout(Vec), /// A chunk of the workload's standard error. @@ -143,7 +143,7 @@ pub enum ExecEvent { /// loses nothing and callers can poll with short deadlines. A failure that leaves the stream /// unusable, such as a reset or a record that the protocol does not allow, ends the session, and /// later calls fail. -pub struct ControlSession { +pub(crate) struct ControlSession { /// The host's NVXS connection to OpenVMM's control endpoint. pub(crate) connection: HostConnection, } @@ -151,7 +151,7 @@ pub struct ControlSession { impl ControlSession { /// Authenticates with the capability OpenVMM received at launch, and waits until `deadline` /// while OpenVMM's broker waits for the guest to acknowledge the session. - pub fn attach( + pub(crate) fn attach( transport: Box, capability: &[u8; CAPABILITY_LEN], deadline: Instant, @@ -162,15 +162,18 @@ impl ControlSession { } /// Waits until the guest agent acknowledges readiness. - pub fn ping(&mut self, deadline: Instant) -> Result<(), SessionError> { + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "SL4.4c's host paths ping the guest through the session" + ) + )] + pub(crate) fn ping(&mut self, deadline: Instant) -> Result<(), SessionError> { self.acknowledged(APP_PING, &[], APP_READY, "readiness", deadline) } /// Asks nvx-init to capture the VM, killing its executions first when requested. - #[cfg_attr( - not(any(test, feature = "cli")), - expect(dead_code, reason = "NVX's capture command requires the cli feature") - )] pub(crate) fn capture( &mut self, kill_executions: bool, @@ -187,7 +190,6 @@ impl ControlSession { /// Sends nvx-init's activate message, whose `payload` carries a restore's activation record /// and workload identity, and returns the activation that the guest's Ready reports. - #[cfg(feature = "cli")] pub(crate) fn activate( &mut self, payload: &[u8], @@ -211,7 +213,14 @@ impl ControlSession { /// /// An agent that predates the request refuses it as an unsupported operation, which means /// that the image provides none of them. - pub fn features(&mut self, deadline: Instant) -> Result { + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "no host path sends FEATURES here, and SL4.4d deletes it" + ) + )] + pub(crate) fn features(&mut self, deadline: Instant) -> Result { let request_id = request_id()?; self.send_app(APP_FEATURES, request_id, &[], Some(deadline))?; let (kind, status, payload) = self.read_reply(request_id, Some(deadline))?; @@ -226,7 +235,7 @@ impl ControlSession { /// Hands the guest agent the host mapping table, in as many requests as it takes, and waits /// until it has mounted every entry. - pub fn map_host_paths( + pub(crate) fn map_host_paths( &mut self, maps: &[HostMap], deadline: Instant, @@ -261,7 +270,14 @@ impl ControlSession { } /// Starts a workload and returns the request ID that identifies its events. - pub fn start_exec( + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "SL4.4c's sandbox exec and run start workloads through it" + ) + )] + pub(crate) fn start_exec( &mut self, workload: &Workload<'_>, deadline: Instant, @@ -273,7 +289,7 @@ impl ControlSession { } /// Reads the next event of the workload started with `request_id`. - pub fn next_exec_event( + pub(crate) fn next_exec_event( &mut self, request_id: u64, deadline: Option, @@ -301,19 +317,37 @@ impl ControlSession { /// The guest reports the workload's outcome as a regular exec event, with the cancelled /// category if the request arrived before the workload exited. A request for a workload that /// already finished is ignored. - pub fn cancel_exec(&mut self, request_id: u64, deadline: Instant) -> Result<(), SessionError> { + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "SL4.4c's sandbox exec and run cancel through it on Ctrl-C" + ) + )] + pub(crate) fn cancel_exec( + &mut self, + request_id: u64, + deadline: Instant, + ) -> Result<(), SessionError> { self.send_app(APP_CANCEL, request_id, &[], Some(deadline)) } /// Asks the guest agent to shut the VM down. - pub fn stop(&mut self, deadline: Instant) -> Result<(), SessionError> { + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "SL4.4c's sandbox exec and run stop the guest through it" + ) + )] + pub(crate) fn stop(&mut self, deadline: Instant) -> Result<(), SessionError> { self.acknowledged(APP_STOP, &[], APP_STOPPED, "stop", deadline) } /// Sends an application request of `kind` with `request_id` and `payload` in the next /// sequenced outer record, for a caller that checks the responses itself, as the `nvx-rs` /// CLI does with the messages of `nvx.py`. - pub fn send_request( + pub(crate) fn send_request( &mut self, kind: u8, request_id: u64, @@ -325,7 +359,7 @@ impl ControlSession { /// Reads the next application frame, checked against the session's identity and sequence, /// and returns its kind, request ID, status, and payload. - pub fn read_response( + pub(crate) fn read_response( &mut self, deadline: Option, ) -> Result<(u8, u64, i32, Vec), SessionError> { diff --git a/nvx-rs/src/host/session/tests.rs b/nvx-rs/src/host/session/tests.rs index e1753b39a..bfdd128e2 100644 --- a/nvx-rs/src/host/session/tests.rs +++ b/nvx-rs/src/host/session/tests.rs @@ -9,6 +9,9 @@ use crate::protocol::{ OUTER_WAIT, WorkloadEnvironment, decode_app, decode_outer_header, encode_app, encode_outer, }; +#[cfg(any(target_os = "linux", windows))] +mod managed_lifecycle; + const INSTANCE: [u8; 16] = [7; 16]; #[test] diff --git a/nvx-rs/tests/managed_lifecycle.rs b/nvx-rs/src/host/session/tests/managed_lifecycle.rs similarity index 99% rename from nvx-rs/tests/managed_lifecycle.rs rename to nvx-rs/src/host/session/tests/managed_lifecycle.rs index 7b1debb8c..a72ef9493 100644 --- a/nvx-rs/tests/managed_lifecycle.rs +++ b/nvx-rs/src/host/session/tests/managed_lifecycle.rs @@ -1,7 +1,7 @@ //! The managed lifecycle of a guest whose init hands the boot to the managed agent, on a real //! hypervisor: authenticated sessions and reconnects, request-scoped working directories and //! environments, the workload identity, timeouts, descendants that outlive their workload, -//! refused working directories, and stop, driven through `nvx::host::session`. +//! refused working directories, and stop, driven through the host's control session. //! //! `scripts/nvx.py test-microvm --scenario managed-lifecycle` runs it for the selected guest and //! backend, then checks the outcome report that OpenVMM wrote and the guest console. It sets: @@ -23,7 +23,6 @@ //! //! These checks were `run_managed_lifecycle` of `scripts/nvx_tools/microvm_tests.py`, which drove //! the control session from Python until microsoft/nvx#359 left the protocol to nvx-rs alone. -#![cfg(all(feature = "host", any(target_os = "linux", windows)))] use std::env; use std::fs::{self, File, OpenOptions}; @@ -35,9 +34,9 @@ use std::sync::mpsc; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use nvx::host::platform::process_start_time; -use nvx::host::session::{self, ControlSession, ExecEvent}; -use nvx::protocol::{ +use crate::host::platform::process_start_time; +use crate::host::session::{self, ControlSession, ExecEvent}; +use crate::protocol::{ self, APP_ERROR, APP_MAPS, APP_READY, CAPABILITY_LEN, CWD_FAILED, ExitCategory, HostMap, Workload, WorkloadEnvironment, }; diff --git a/nvx-rs/src/host/transport/tests.rs b/nvx-rs/src/host/transport/tests.rs index 5c0716802..b49065f54 100644 --- a/nvx-rs/src/host/transport/tests.rs +++ b/nvx-rs/src/host/transport/tests.rs @@ -1,6 +1,7 @@ #![cfg(any(target_os = "linux", windows))] use std::error::Error as _; +#[cfg(feature = "cli")] use std::io::{Read, Write}; use std::process::{Child, Command, Stdio}; @@ -52,6 +53,7 @@ fn bind(endpoint: &str) -> Server { } } +#[cfg(feature = "cli")] fn exchange(server: Server) { #[cfg(target_os = "linux")] let mut peer = server.accept().unwrap().0; @@ -96,6 +98,7 @@ impl Drop for Sleeper { } } +#[cfg(feature = "cli")] #[test] fn verified_and_legacy_connections_wait_for_the_owned_endpoint_and_keep_native_io() { let process = reference(std::process::id()); @@ -138,6 +141,7 @@ fn verified_and_legacy_connections_wait_for_the_owned_endpoint_and_keep_native_i } } +#[cfg(feature = "cli")] #[test] fn another_process_serving_the_endpoint_is_refused() { let vm = Sleeper::new(); diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index 4e078bed5..0c2d03951 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -32,8 +32,8 @@ //! the host and the guest share it. //! - [`Hypervisor`]: the hypervisors that run the VMs. //! - [`host`] (feature `host`): artifact discovery, the OpenVMM command line, the lowering of -//! network and filesystem policies, the host's NVXS connection, control sessions, process and -//! platform primitives, VMs that a caller creates, starts, reattaches to, and stops in private +//! network and filesystem policies, the host's NVXS connection, process and platform +//! primitives, VMs that a caller creates, starts, reattaches to, and stops in private //! directories that it owns, and the manifests and compatibility keys of templates. //! - `guest` (feature `guest`, Linux): the programs that run in NVX guests, such as //! `nvx-console-pending`, the guest's NVXS connection, the libraries through which any guest diff --git a/nvx-rs/src/protocol/app.rs b/nvx-rs/src/protocol/app.rs index 872522658..fd8295aa3 100644 --- a/nvx-rs/src/protocol/app.rs +++ b/nvx-rs/src/protocol/app.rs @@ -54,10 +54,13 @@ pub const APP_MAPS: u8 = 6; #[cfg_attr( not(any( test, - feature = "host", + feature = "cli", all(feature = "guest", target_os = "linux", target_arch = "x86_64") )), - expect(dead_code, reason = "SL7.7a makes nvx-init handle capture requests") + expect( + dead_code, + reason = "only the CLI's capture command sends it, and only nvx-init reads it" + ) )] pub(crate) const APP_CAPTURE: u8 = 7; /// Request that nvx-init activate a restored VM with its record and workload identity. @@ -159,8 +162,11 @@ pub fn decode_app(frame: &[u8]) -> Result, ProtocolError> { /// Encodes the capture request's flag as one byte, 0 or 1. #[cfg_attr( - not(any(test, feature = "host")), - expect(dead_code, reason = "SL7.7a makes nvx-init handle capture requests") + not(any(test, feature = "cli")), + expect( + dead_code, + reason = "only the CLI's capture command sends capture requests" + ) )] pub(crate) fn encode_capture_payload(kill_executions: bool) -> [u8; 1] { [u8::from(kill_executions)] diff --git a/nvx-rs/src/protocol/mod.rs b/nvx-rs/src/protocol/mod.rs index 4b82adb3a..9d5dc173e 100644 --- a/nvx-rs/src/protocol/mod.rs +++ b/nvx-rs/src/protocol/mod.rs @@ -62,13 +62,13 @@ pub mod view; ))] pub(crate) use app::APP_ACTIVATE; #[cfg(any( - feature = "host", + feature = "cli", all(feature = "guest", target_os = "linux", target_arch = "x86_64") ))] pub(crate) use app::APP_CAPTURE; #[cfg(feature = "cli")] pub(crate) use app::encode_activate_payload; -#[cfg(feature = "host")] +#[cfg(feature = "cli")] pub(crate) use app::encode_capture_payload; #[cfg(any( feature = "host", diff --git a/scripts/nvx_tools/microvm_tests.py b/scripts/nvx_tools/microvm_tests.py index b07015b4b..e49da312e 100644 --- a/scripts/nvx_tools/microvm_tests.py +++ b/scripts/nvx_tools/microvm_tests.py @@ -1359,8 +1359,13 @@ def run_workload_identity( # The ignored nvx-rs test that drives the managed lifecycle of a guest whose -# init hands the boot to the managed agent: nvx-rs/tests/managed_lifecycle.rs. -MANAGED_LIFECYCLE_TEST = "managed_lifecycle" +# init hands the boot to the managed agent: a unit test of the host's control +# session, which is private to nvx and which the cli feature builds, in +# nvx-rs/src/host/session/tests/managed_lifecycle.rs. +MANAGED_LIFECYCLE_TEST = ( + "host::session::tests::managed_lifecycle::" + "a_managed_guest_runs_request_scoped_workloads_until_it_stops" +) def ignored_nvx_rs_test_command(cargo: str, test: str) -> list[str]: @@ -1381,8 +1386,23 @@ def ignored_nvx_rs_test_command(cargo: str, test: str) -> list[str]: def managed_lifecycle_test_command(cargo: str) -> list[str]: - """Return the Cargo command that runs MANAGED_LIFECYCLE_TEST.""" - return ignored_nvx_rs_test_command(cargo, MANAGED_LIFECYCLE_TEST) + """Return the Cargo command that runs MANAGED_LIFECYCLE_TEST, an ignored + unit test of nvx-rs's library, with the cli feature.""" + return [ + cargo, + "test", + "--manifest-path", + os.fspath(nvx_rs.DIRECTORY / "Cargo.toml"), + "--locked", + "--features", + "cli", + "--lib", + "--", + "--ignored", + "--nocapture", + "--exact", + MANAGED_LIFECYCLE_TEST, + ] def run_managed_lifecycle( diff --git a/scripts/nvx_tools/sandbox_lifecycle_tests.py b/scripts/nvx_tools/sandbox_lifecycle_tests.py index 5ee5b6db7..ffc35e348 100644 --- a/scripts/nvx_tools/sandbox_lifecycle_tests.py +++ b/scripts/nvx_tools/sandbox_lifecycle_tests.py @@ -315,9 +315,10 @@ def check_managed_report( """Checks the report of a managed sandbox that stopped cleanly. These are the report checks of the direct-mode managed lifecycle - (nvx-rs/tests/managed_lifecycle.rs) that hold for the layered root too: the - report names the backend, reports `network_policy`, or that no network - policy was requested when that is None, and every teardown step completed. + (nvx-rs/src/host/session/tests/managed_lifecycle.rs) that hold for the + layered root too: the report names the backend, reports `network_policy`, + or that no network policy was requested when that is None, and every + teardown step completed. """ outcome = report.get("outcome") if outcome != MANAGED_SUCCESS: diff --git a/scripts/test-layout-allowlist.json b/scripts/test-layout-allowlist.json index 76a11780e..31193c0d5 100644 --- a/scripts/test-layout-allowlist.json +++ b/scripts/test-layout-allowlist.json @@ -13,7 +13,7 @@ "nvx-rs/src/guest/time/system.rs": 3, "nvx-rs/src/host/network/address.rs": 2, "nvx-rs/src/host/platform/windows/spawn.rs": 1, - "nvx-rs/src/host/session.rs": 1, + "nvx-rs/src/host/session.rs": 5, "nvx-rs/src/host/vm.rs": 2, "nvx-rs/src/protocol/app.rs": 6 } diff --git a/scripts/test_microvm_tests.py b/scripts/test_microvm_tests.py index ee395bba2..48a8ce11d 100644 --- a/scripts/test_microvm_tests.py +++ b/scripts/test_microvm_tests.py @@ -5691,7 +5691,8 @@ def start_test( return runs def test_managed_lifecycle_runs_the_nvx_rs_test_and_checks_what_it_leaves(self): - # nvx-rs/tests/managed_lifecycle.rs drives the control session; the + # The unit test of nvx-rs's host control session in + # src/host/session/tests/managed_lifecycle.rs drives the session; the # scenario hands it OpenVMM's command, then checks the outcome report # and the guest console. with tempfile.TemporaryDirectory() as temporary: @@ -5722,11 +5723,15 @@ def test_managed_lifecycle_runs_the_nvx_rs_test_and_checks_what_it_leaves(self): "--manifest-path", os.fspath(BuildConstants.REPO_ROOT / "nvx-rs" / "Cargo.toml"), "--locked", - "--test", - "managed_lifecycle", + "--features", + "cli", + "--lib", "--", "--ignored", "--nocapture", + "--exact", + "host::session::tests::managed_lifecycle::" + "a_managed_guest_runs_request_scoped_workloads_until_it_stops", ], ) # The repository root selects the pinned Rust release. @@ -5791,8 +5796,9 @@ def test_managed_lifecycle_requires_cargo(self): patch.object(microvm_tests.subprocess, "Popen") as popen, self.assertRaisesRegex( common.ScriptError, - "runs the nvx-rs test managed_lifecycle with Cargo, which is not " - "on PATH", + "runs the nvx-rs test host::session::tests::managed_lifecycle::" + "a_managed_guest_runs_request_scoped_workloads_until_it_stops with " + "Cargo, which is not on PATH", ), ): microvm_tests.run_managed_lifecycle( @@ -5854,8 +5860,9 @@ def test_managed_lifecycle_fails_with_the_test_and_with_the_report(self): ( {"returncode": 101}, RuntimeError, - "the managed lifecycle test managed_lifecycle failed with exit " - "status 101", + "the managed lifecycle test host::session::tests::managed_lifecycle::" + "a_managed_guest_runs_request_scoped_workloads_until_it_stops failed " + "with exit status 101", ), # The guest's own explanation comes before the test's failure. ( From 0e927f7865af85ec931dc6af78decbff17642a1e Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 22:44:17 -0800 Subject: [PATCH 4/4] protocol: make NVXC private to the programs that speak it nvx::protocol re-exported NVXC, NVX's own control protocol, as public API of nvx: its application frames (AppFrame, encode_app, decode_app and the APP_* kinds), the refusal categories and ExitCategory, the EXEC payloads with their codecs, flags and limits, and GuestFeatures. Only NVX's programs speak NVXC: the CLI and its restore driver, nvx-init and the managed agent, and the fake OpenVMM. SL6.12 (#1127) makes it crate-private where it is, before NVXC v2 changes it. Every NVXC item becomes pub(crate) and is gated by the features of the code that uses it, so that no feature set builds an item that it does not use: cli for the host's encoders and checks, guest on Linux x86-64 for the guest's decoders and the managed agent, and testing for the fake OpenVMM's decoder. The exec and features modules compile with any of them. protocol re-exports each item under the gate of its users, and no longer re-exports what only the codec itself reads. The capture and activate messages, whose expect(dead_code) hooks covered the builds without their users, take the same gates, so app.rs holds no test hook any more and its allowlist entry goes. GuestFeatures's REQUIRED and contains, MAX_CWD_BYTES and MAX_TIMEOUT_MS, which no feature set uses, move into protocol's tests, which alone read them. protocol::exec is crate-visible, for the CLI session's tests that read the EXEC flags. The tests of NVXC are gated by the features of the code that they test, with their bodies unchanged, so that every build compiles them where their code is built, and every one of them runs in the all-features tests on Linux: protocol's NVXC tests take the gates of the codecs that they check; the managed agent's dispatch tests, which drive the agent with the host's EXEC encoder, and the test helpers that only they use, take cli; and tests of the host's session, the CLI's session and the fake OpenVMM that use a guest or testing constant take that feature. The managed lifecycle test spells the three refusal categories that it expects, since the cli feature alone, with which its scenario runs it, does not build the guest's constants. The public-API snapshots drop the 93 NVXC items of each platform, and only those: NVXS, the outer records and the mapping table's payloads and tokens stay. protocol's docs, the crate's, the README and the items that linked NVXC no longer present it as public API. Part of #1127 (SL6.12). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/acceptance-case-matrix.md | 4 +- nvx-rs/README.md | 10 +- nvx-rs/public-api/linux.txt | 93 ------ nvx-rs/public-api/windows.txt | 93 ------ nvx-rs/src/guest/managed_agent/dispatch.rs | 2 +- nvx-rs/src/guest/managed_agent/seam/tests.rs | 3 + nvx-rs/src/guest/managed_agent/tests.rs | 1 + nvx-rs/src/guest/sys/spawn/tests.rs | 4 + nvx-rs/src/guest/sys/tests.rs | 4 +- nvx-rs/src/host/cli/session/tests.rs | 12 +- nvx-rs/src/host/session/tests.rs | 5 + .../host/session/tests/managed_lifecycle.rs | 26 +- nvx-rs/src/lib.rs | 9 +- nvx-rs/src/protocol/app.rs | 271 ++++++++++++------ nvx-rs/src/protocol/exec.rs | 123 +++++--- nvx-rs/src/protocol/features.rs | 68 +++-- nvx-rs/src/protocol/maps.rs | 4 +- nvx-rs/src/protocol/mod.rs | 77 +++-- nvx-rs/src/protocol/tests.rs | 252 +++++++++++++++- nvx-rs/src/testing/fake_openvmm.rs | 2 +- nvx-rs/src/testing/fake_openvmm/tests.rs | 6 + scripts/test-layout-allowlist.json | 3 +- 22 files changed, 684 insertions(+), 388 deletions(-) diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index 422701095..9ac21e0fe 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -504,9 +504,9 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/host/template/plan.rs - host::template::plan::TemplatePlan (line 137)` | retired | `TemplatePlan` leaves with the template store (SL6.2a, #1121) | | `nvx-rs/src/host/template/store.rs - host::template::store::Store (line 473)` | retired | The template store leaves (SL6.2a, #1121) | | `nvx-rs/src/host/transport.rs - host::transport (line 22)` | kept | | -| `nvx-rs/src/lib.rs - (line 58)` | rewritten | Its `Instance` and sandbox store leave (SL6.1, #1121): `nvx-rs/src/lib.rs - (line 61)` compiles a cold boot and a stop through `host::vm` | +| `nvx-rs/src/lib.rs - (line 58)` | rewritten | Its `Instance` and sandbox store leave (SL6.1, #1121): `nvx-rs/src/lib.rs - (line 62)` compiles a cold boot and a stop through `host::vm` | | `nvx-rs/src/protocol/nvxs/mod.rs - protocol::nvxs (line 68)` | kept | | | `nvx-rs/src/host/snapshot/capture.rs - host::snapshot::capture::finish_capture (line 48)` | kept | | | `nvx-rs/src/host/store/mod.rs - host::store (line 104)` | retired | The sandbox store leaves (SL6.1, #1121) | -| `nvx-rs/README.md`, its Example: an instance through `LaunchSpec` and the sandbox store | rewritten | Nothing compiles it. `nvx-rs/examples/host-workflows.rs` compiles the documented host workflows, and the library's guide (SL6.10a) replaces the example. SL6.1 (#1121) made it the cold boot that `nvx-rs/src/lib.rs - (line 61)` compiles | +| `nvx-rs/README.md`, its Example: an instance through `LaunchSpec` and the sandbox store | rewritten | Nothing compiles it. `nvx-rs/examples/host-workflows.rs` compiles the documented host workflows, and the library's guide (SL6.10a) replaces the example. SL6.1 (#1121) made it the cold boot that `nvx-rs/src/lib.rs - (line 62)` compiles | | `nvx-rs/examples/external-launch.rs` | kept | Compiled with the crate's targets, and run by hand on a host | diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 92625b9e6..9a4e6edbf 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -100,7 +100,7 @@ changes only when the module's purpose does. | Module | Purpose | | --- | --- | -| [`nvx::protocol`](#nvxprotocol) | The codec of the control console's `NVXS` records, `NVXC` frames, and payloads, which the host, the guest agent, and the fake OpenVMM share | +| [`nvx::protocol`](#nvxprotocol) | The codec of the control console's `NVXS` records and the payloads that the host, the guest agent, and the fake OpenVMM share | | [`nvx::protocol::cmdline`](#nvxprotocolcmdline) | The kernel command line as Linux and the guest's init read it, with the virtio-mmio devices and the guest's DNS server that it names | | [`nvx::protocol::export`](#nvxprotocolexport) | The boot tokens of the host export, the virtio-fs device from which a guest maps host paths, and the guest's parser of them | | [`nvx::protocol::nvxs`](#nvxprotocolnvxs) | The NVXS session, independent of any application protocol: its records and the state machines of its host and guest roles | @@ -163,10 +163,12 @@ fields. ### `nvx::protocol` -The codec of the control console: `NVXS` outer records, `NVXC` application +The codec of the control console: `NVXS` outer records, the `MAPS` payloads of +the host mapping table, and the `nvx_maps=` and `nvx_workload_account=create` +tokens. NVX's own control protocol, `NVXC`, is private to the crate, gated by +the features of its users, `cli`, `guest` and `testing`: its application frames, the `EXEC` payload that the host encodes and the guest agent decodes, -the `MAPS` payloads of the host mapping table, `GuestFeatures`, and the -`nvx_maps=` and `nvx_workload_account=create` tokens. +and the control features that a guest image advertises. The [conformance vectors](tests/vectors/README.md) of version 1, `tests/vectors/nvxc-v1.txt`, and OpenVMM's vectors and cases of the `NVXS` diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index cce400036..2967ea292 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3150,72 +3150,7 @@ fn nvx::net::guest_gateway fn nvx::net::is_valid_guest_network fn nvx::net::is_valid_hostname mod nvx::protocol -const nvx::protocol::APP_CANCEL -const nvx::protocol::APP_ERROR -const nvx::protocol::APP_EXEC -const nvx::protocol::APP_EXIT -const nvx::protocol::APP_FEATURES -const nvx::protocol::APP_HEADER_LEN -const nvx::protocol::APP_MAGIC -const nvx::protocol::APP_MAPS -const nvx::protocol::APP_PING -const nvx::protocol::APP_READY -const nvx::protocol::APP_STDERR -const nvx::protocol::APP_STDOUT -const nvx::protocol::APP_STOP -const nvx::protocol::APP_STOPPED -const nvx::protocol::APP_VERSION -struct nvx::protocol::AppFrame -field nvx::protocol::AppFrame::kind -field nvx::protocol::AppFrame::payload -field nvx::protocol::AppFrame::request_id -field nvx::protocol::AppFrame::status -const nvx::protocol::BUSY const nvx::protocol::CAPABILITY_LEN -const nvx::protocol::CWD_FAILED -const nvx::protocol::EXEC_CWD_PRESENT -const nvx::protocol::EXEC_ENVIRONMENT_PRESENT -const nvx::protocol::EXEC_EXTENDED -const nvx::protocol::EXEC_INHERIT_DEFAULT_ENV -struct nvx::protocol::ExecPayload -field nvx::protocol::ExecPayload::argv -field nvx::protocol::ExecPayload::cwd -field nvx::protocol::ExecPayload::environment -field nvx::protocol::ExecPayload::inherit_default_env -field nvx::protocol::ExecPayload::timeout_ms -enum nvx::protocol::ExitCategory -variant nvx::protocol::ExitCategory::Cancelled -variant nvx::protocol::ExitCategory::Exit -variant nvx::protocol::ExitCategory::Failed -variant nvx::protocol::ExitCategory::OutputLimit -variant nvx::protocol::ExitCategory::Signal -variant nvx::protocol::ExitCategory::Timeout -method nvx::protocol::ExitCategory::as_str -method nvx::protocol::ExitCategory::parse -struct nvx::protocol::GuestExecPayload -field nvx::protocol::GuestExecPayload::argv -field nvx::protocol::GuestExecPayload::cwd -field nvx::protocol::GuestExecPayload::environment -field nvx::protocol::GuestExecPayload::inherit_default_env -field nvx::protocol::GuestExecPayload::timeout_ms -struct nvx::protocol::GuestFeatures -const nvx::protocol::GuestFeatures::CANCEL -const nvx::protocol::GuestFeatures::DIRECT_AGENT -const nvx::protocol::GuestFeatures::EXEC_CGROUP -const nvx::protocol::GuestFeatures::EXEC_CWD -const nvx::protocol::GuestFeatures::EXEC_ENVIRONMENT -const nvx::protocol::GuestFeatures::HOST_MAPPING_TABLE -const nvx::protocol::GuestFeatures::LAYERED_SANDBOX -const nvx::protocol::GuestFeatures::NONE -const nvx::protocol::GuestFeatures::REQUIRED -const nvx::protocol::GuestFeatures::SANDBOX_AGENT -const nvx::protocol::GuestFeatures::WORKLOAD_ACCOUNT -method nvx::protocol::GuestFeatures::bits -method nvx::protocol::GuestFeatures::contains -method nvx::protocol::GuestFeatures::decode -method nvx::protocol::GuestFeatures::encode -method nvx::protocol::GuestFeatures::from_bits -method nvx::protocol::GuestFeatures::missing struct nvx::protocol::GuestHostMap field nvx::protocol::GuestHostMap::read_only field nvx::protocol::GuestHostMap::source @@ -3227,23 +3162,12 @@ struct nvx::protocol::HostMap field nvx::protocol::HostMap::read_only field nvx::protocol::HostMap::source field nvx::protocol::HostMap::target -const nvx::protocol::INVALID_REQUEST -const nvx::protocol::LAUNCH_FAILED -const nvx::protocol::MAPPINGS_INCOMPLETE -const nvx::protocol::MAPPING_FAILED const nvx::protocol::MAPS_HEADER_LEN const nvx::protocol::MAPS_TOKEN const nvx::protocol::MAP_ENTRY_HEADER_LEN const nvx::protocol::MAP_READ_ONLY -const nvx::protocol::MAX_ARGUMENTS -const nvx::protocol::MAX_ARGUMENT_BYTES -const nvx::protocol::MAX_CWD_BYTES -const nvx::protocol::MAX_ENVIRONMENT -const nvx::protocol::MAX_GUEST_CWD_BYTES const nvx::protocol::MAX_HOST_MAPPINGS const nvx::protocol::MAX_MAP_PATH_BYTES -const nvx::protocol::MAX_OUTPUT_BYTES -const nvx::protocol::MAX_TIMEOUT_MS struct nvx::protocol::MapsPayload field nvx::protocol::MapsPayload::first field nvx::protocol::MapsPayload::maps @@ -3272,19 +3196,7 @@ field nvx::protocol::OuterHeader::sequence struct nvx::protocol::ProtocolError method nvx::protocol::ProtocolError::into_message method nvx::protocol::ProtocolError::message -const nvx::protocol::UNSUPPORTED_OPERATION const nvx::protocol::WORKLOAD_ACCOUNT_TOKEN -struct nvx::protocol::Workload -field nvx::protocol::Workload::argv -field nvx::protocol::Workload::cwd -field nvx::protocol::Workload::environment -field nvx::protocol::Workload::timeout_ms -enum nvx::protocol::WorkloadEnvironment -variant nvx::protocol::WorkloadEnvironment::Default -variant nvx::protocol::WorkloadEnvironment::Layered -field nvx::protocol::WorkloadEnvironment::Layered::0 -variant nvx::protocol::WorkloadEnvironment::Replaced -field nvx::protocol::WorkloadEnvironment::Replaced::0 mod nvx::protocol::cmdline const nvx::protocol::cmdline::DNS_TOKEN const nvx::protocol::cmdline::GATEWAY_TOKEN @@ -3311,17 +3223,12 @@ fn nvx::protocol::cmdline::parameters fn nvx::protocol::cmdline::virtio_mmio_devices fn nvx::protocol::cmdline::write_resolver fn nvx::protocol::decode_ack_payload -fn nvx::protocol::decode_app fn nvx::protocol::decode_credit_payload -fn nvx::protocol::decode_exec_payload -fn nvx::protocol::decode_guest_exec_payload fn nvx::protocol::decode_guest_maps_payload fn nvx::protocol::decode_maps_payload fn nvx::protocol::decode_outer_header fn nvx::protocol::encode_ack_payload -fn nvx::protocol::encode_app fn nvx::protocol::encode_credit_payload -fn nvx::protocol::encode_exec_payload fn nvx::protocol::encode_maps_payloads fn nvx::protocol::encode_outer mod nvx::protocol::export diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 14a0154a3..de5f1ecc5 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1752,72 +1752,7 @@ fn nvx::net::guest_gateway fn nvx::net::is_valid_guest_network fn nvx::net::is_valid_hostname mod nvx::protocol -const nvx::protocol::APP_CANCEL -const nvx::protocol::APP_ERROR -const nvx::protocol::APP_EXEC -const nvx::protocol::APP_EXIT -const nvx::protocol::APP_FEATURES -const nvx::protocol::APP_HEADER_LEN -const nvx::protocol::APP_MAGIC -const nvx::protocol::APP_MAPS -const nvx::protocol::APP_PING -const nvx::protocol::APP_READY -const nvx::protocol::APP_STDERR -const nvx::protocol::APP_STDOUT -const nvx::protocol::APP_STOP -const nvx::protocol::APP_STOPPED -const nvx::protocol::APP_VERSION -struct nvx::protocol::AppFrame -field nvx::protocol::AppFrame::kind -field nvx::protocol::AppFrame::payload -field nvx::protocol::AppFrame::request_id -field nvx::protocol::AppFrame::status -const nvx::protocol::BUSY const nvx::protocol::CAPABILITY_LEN -const nvx::protocol::CWD_FAILED -const nvx::protocol::EXEC_CWD_PRESENT -const nvx::protocol::EXEC_ENVIRONMENT_PRESENT -const nvx::protocol::EXEC_EXTENDED -const nvx::protocol::EXEC_INHERIT_DEFAULT_ENV -struct nvx::protocol::ExecPayload -field nvx::protocol::ExecPayload::argv -field nvx::protocol::ExecPayload::cwd -field nvx::protocol::ExecPayload::environment -field nvx::protocol::ExecPayload::inherit_default_env -field nvx::protocol::ExecPayload::timeout_ms -enum nvx::protocol::ExitCategory -variant nvx::protocol::ExitCategory::Cancelled -variant nvx::protocol::ExitCategory::Exit -variant nvx::protocol::ExitCategory::Failed -variant nvx::protocol::ExitCategory::OutputLimit -variant nvx::protocol::ExitCategory::Signal -variant nvx::protocol::ExitCategory::Timeout -method nvx::protocol::ExitCategory::as_str -method nvx::protocol::ExitCategory::parse -struct nvx::protocol::GuestExecPayload -field nvx::protocol::GuestExecPayload::argv -field nvx::protocol::GuestExecPayload::cwd -field nvx::protocol::GuestExecPayload::environment -field nvx::protocol::GuestExecPayload::inherit_default_env -field nvx::protocol::GuestExecPayload::timeout_ms -struct nvx::protocol::GuestFeatures -const nvx::protocol::GuestFeatures::CANCEL -const nvx::protocol::GuestFeatures::DIRECT_AGENT -const nvx::protocol::GuestFeatures::EXEC_CGROUP -const nvx::protocol::GuestFeatures::EXEC_CWD -const nvx::protocol::GuestFeatures::EXEC_ENVIRONMENT -const nvx::protocol::GuestFeatures::HOST_MAPPING_TABLE -const nvx::protocol::GuestFeatures::LAYERED_SANDBOX -const nvx::protocol::GuestFeatures::NONE -const nvx::protocol::GuestFeatures::REQUIRED -const nvx::protocol::GuestFeatures::SANDBOX_AGENT -const nvx::protocol::GuestFeatures::WORKLOAD_ACCOUNT -method nvx::protocol::GuestFeatures::bits -method nvx::protocol::GuestFeatures::contains -method nvx::protocol::GuestFeatures::decode -method nvx::protocol::GuestFeatures::encode -method nvx::protocol::GuestFeatures::from_bits -method nvx::protocol::GuestFeatures::missing struct nvx::protocol::GuestHostMap field nvx::protocol::GuestHostMap::read_only field nvx::protocol::GuestHostMap::source @@ -1829,23 +1764,12 @@ struct nvx::protocol::HostMap field nvx::protocol::HostMap::read_only field nvx::protocol::HostMap::source field nvx::protocol::HostMap::target -const nvx::protocol::INVALID_REQUEST -const nvx::protocol::LAUNCH_FAILED -const nvx::protocol::MAPPINGS_INCOMPLETE -const nvx::protocol::MAPPING_FAILED const nvx::protocol::MAPS_HEADER_LEN const nvx::protocol::MAPS_TOKEN const nvx::protocol::MAP_ENTRY_HEADER_LEN const nvx::protocol::MAP_READ_ONLY -const nvx::protocol::MAX_ARGUMENTS -const nvx::protocol::MAX_ARGUMENT_BYTES -const nvx::protocol::MAX_CWD_BYTES -const nvx::protocol::MAX_ENVIRONMENT -const nvx::protocol::MAX_GUEST_CWD_BYTES const nvx::protocol::MAX_HOST_MAPPINGS const nvx::protocol::MAX_MAP_PATH_BYTES -const nvx::protocol::MAX_OUTPUT_BYTES -const nvx::protocol::MAX_TIMEOUT_MS struct nvx::protocol::MapsPayload field nvx::protocol::MapsPayload::first field nvx::protocol::MapsPayload::maps @@ -1874,19 +1798,7 @@ field nvx::protocol::OuterHeader::sequence struct nvx::protocol::ProtocolError method nvx::protocol::ProtocolError::into_message method nvx::protocol::ProtocolError::message -const nvx::protocol::UNSUPPORTED_OPERATION const nvx::protocol::WORKLOAD_ACCOUNT_TOKEN -struct nvx::protocol::Workload -field nvx::protocol::Workload::argv -field nvx::protocol::Workload::cwd -field nvx::protocol::Workload::environment -field nvx::protocol::Workload::timeout_ms -enum nvx::protocol::WorkloadEnvironment -variant nvx::protocol::WorkloadEnvironment::Default -variant nvx::protocol::WorkloadEnvironment::Layered -field nvx::protocol::WorkloadEnvironment::Layered::0 -variant nvx::protocol::WorkloadEnvironment::Replaced -field nvx::protocol::WorkloadEnvironment::Replaced::0 mod nvx::protocol::cmdline const nvx::protocol::cmdline::DNS_TOKEN const nvx::protocol::cmdline::GATEWAY_TOKEN @@ -1913,17 +1825,12 @@ fn nvx::protocol::cmdline::parameters fn nvx::protocol::cmdline::virtio_mmio_devices fn nvx::protocol::cmdline::write_resolver fn nvx::protocol::decode_ack_payload -fn nvx::protocol::decode_app fn nvx::protocol::decode_credit_payload -fn nvx::protocol::decode_exec_payload -fn nvx::protocol::decode_guest_exec_payload fn nvx::protocol::decode_guest_maps_payload fn nvx::protocol::decode_maps_payload fn nvx::protocol::decode_outer_header fn nvx::protocol::encode_ack_payload -fn nvx::protocol::encode_app fn nvx::protocol::encode_credit_payload -fn nvx::protocol::encode_exec_payload fn nvx::protocol::encode_maps_payloads fn nvx::protocol::encode_outer mod nvx::protocol::export diff --git a/nvx-rs/src/guest/managed_agent/dispatch.rs b/nvx-rs/src/guest/managed_agent/dispatch.rs index 0693028a4..d047af7c6 100644 --- a/nvx-rs/src/guest/managed_agent/dispatch.rs +++ b/nvx-rs/src/guest/managed_agent/dispatch.rs @@ -315,5 +315,5 @@ fn handle_data( Ok(None) } -#[cfg(test)] +#[cfg(all(test, feature = "cli"))] mod tests; diff --git a/nvx-rs/src/guest/managed_agent/seam/tests.rs b/nvx-rs/src/guest/managed_agent/seam/tests.rs index 0319e3eb3..574203b6d 100644 --- a/nvx-rs/src/guest/managed_agent/seam/tests.rs +++ b/nvx-rs/src/guest/managed_agent/seam/tests.rs @@ -55,6 +55,7 @@ impl TestSystem { } /// Returns a direct agent with no cgroup hierarchy in its temporary directory. + #[cfg(feature = "cli")] pub(crate) fn with_direct() -> (tempfile::TempDir, Self) { let temporary = tempfile::tempdir().unwrap(); let root = temporary.path().join("cgroup"); @@ -200,6 +201,7 @@ impl System for TestSystem { /// Starts a native direct workload as the privileged agent does, without privileges: the child's /// credential calls are replaced and succeed, and it enters the working directory with the test's /// own credentials. +#[cfg(feature = "cli")] pub(crate) fn start_as_privileged( request: init::ExecRequest, ) -> Result { @@ -207,6 +209,7 @@ pub(crate) fn start_as_privileged( } /// Starts a native direct workload whose simulated identity stage fails with `EPERM`. +#[cfg(feature = "cli")] pub(crate) fn start_with_failing_credentials( request: init::ExecRequest, ) -> Result { diff --git a/nvx-rs/src/guest/managed_agent/tests.rs b/nvx-rs/src/guest/managed_agent/tests.rs index 5f3f6f9bc..7f1bae486 100644 --- a/nvx-rs/src/guest/managed_agent/tests.rs +++ b/nvx-rs/src/guest/managed_agent/tests.rs @@ -8,6 +8,7 @@ pub(crate) use crate::guest::sys::tests::isolate_process_test; mod loopback; /// Serializes tests that temporarily alter process-global state. +#[cfg(feature = "cli")] pub(crate) static PROCESS_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); impl AgentConfig { diff --git a/nvx-rs/src/guest/sys/spawn/tests.rs b/nvx-rs/src/guest/sys/spawn/tests.rs index 443e24d11..b22de6243 100644 --- a/nvx-rs/src/guest/sys/spawn/tests.rs +++ b/nvx-rs/src/guest/sys/spawn/tests.rs @@ -5,15 +5,19 @@ use super::*; use crate::guest::sys::{self, ChildStatus}; /// Capability to bypass file read, write, and execute permission checks. +#[cfg(feature = "cli")] pub(crate) const CAP_DAC_OVERRIDE: u32 = 1; /// Capability to bypass file read permission and directory read and search permission checks. +#[cfg(feature = "cli")] pub(crate) const CAP_DAC_READ_SEARCH: u32 = 2; /// Capability to change group IDs and supplementary groups. +#[cfg(feature = "cli")] pub(crate) const CAP_SETGID: u32 = 6; /// Capability to change user IDs. +#[cfg(feature = "cli")] pub(crate) const CAP_SETUID: u32 = 7; /// Capability to change the root directory. diff --git a/nvx-rs/src/guest/sys/tests.rs b/nvx-rs/src/guest/sys/tests.rs index c877aa249..4569f4bcb 100644 --- a/nvx-rs/src/guest/sys/tests.rs +++ b/nvx-rs/src/guest/sys/tests.rs @@ -23,9 +23,11 @@ pub(crate) use super::rooted::tests::{ }; pub(crate) use super::signal::tests::{default_sigpipe, has_child, reap_children_automatically}; pub(crate) use super::sock_diag::tests::{encode_inet_diag_done, encode_inet_diag_socket}; +#[cfg(feature = "cli")] pub(crate) use super::spawn::tests::{ - CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_SETGID, CAP_SETUID, CAP_SYS_ADMIN, holds_capability, + CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_SETGID, CAP_SETUID, }; +pub(crate) use super::spawn::tests::{CAP_SYS_ADMIN, holds_capability}; #[cfg(target_arch = "x86_64")] pub(crate) use super::spawn::tests::{CAP_SYS_NICE, CAP_SYS_RESOURCE}; diff --git a/nvx-rs/src/host/cli/session/tests.rs b/nvx-rs/src/host/cli/session/tests.rs index 34882c923..9c616c3e5 100644 --- a/nvx-rs/src/host/cli/session/tests.rs +++ b/nvx-rs/src/host/cli/session/tests.rs @@ -9,8 +9,10 @@ use std::time::Instant; use super::*; use crate::host::transport::HostConnection; -use crate::protocol::{ +use crate::protocol::exec::{ EXEC_CWD_PRESENT, EXEC_ENVIRONMENT_PRESENT, EXEC_EXTENDED, EXEC_INHERIT_DEFAULT_ENV, +}; +use crate::protocol::{ OUTER_DATA, OUTER_HEADER_LEN, OUTER_HOST_ATTACH, OUTER_READY, OUTER_RESET, decode_app, decode_outer_header, encode_app, encode_outer, }; @@ -1062,6 +1064,10 @@ fn output_beyond_the_guest_limit_breaks_the_protocol() { worker.join().unwrap(); } +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn features_of_a_guest_that_predates_them_are_none() { let (mut session, worker) = session_with(|guest| { @@ -1087,6 +1093,10 @@ fn features_of_a_guest_that_predates_them_are_none() { worker.join().unwrap(); } +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn protocol_violations_keep_the_messages_of_python() { let (mut session, worker) = session_with(|guest| { diff --git a/nvx-rs/src/host/session/tests.rs b/nvx-rs/src/host/session/tests.rs index bfdd128e2..74f63c441 100644 --- a/nvx-rs/src/host/session/tests.rs +++ b/nvx-rs/src/host/session/tests.rs @@ -354,6 +354,10 @@ fn exec_streams_events_in_order() { ); } +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn exec_sends_the_working_directory_and_reports_its_refusal() { let (mut session, wire) = ready(); @@ -662,6 +666,7 @@ fn activate_refuses_a_reply_other_than_a_valid_ready() { } } +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] #[test] fn features_report_what_the_guest_provides() { let mut session = attached(|kind, id| { diff --git a/nvx-rs/src/host/session/tests/managed_lifecycle.rs b/nvx-rs/src/host/session/tests/managed_lifecycle.rs index a72ef9493..be185de6c 100644 --- a/nvx-rs/src/host/session/tests/managed_lifecycle.rs +++ b/nvx-rs/src/host/session/tests/managed_lifecycle.rs @@ -37,10 +37,20 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use crate::host::platform::process_start_time; use crate::host::session::{self, ControlSession, ExecEvent}; use crate::protocol::{ - self, APP_ERROR, APP_MAPS, APP_READY, CAPABILITY_LEN, CWD_FAILED, ExitCategory, HostMap, - Workload, WorkloadEnvironment, + self, APP_ERROR, APP_MAPS, APP_READY, CAPABILITY_LEN, ExitCategory, HostMap, Workload, + WorkloadEnvironment, }; +/// The categories with which the guest refuses a working directory that the workload cannot +/// enter, a malformed request, and a workload before it applied every host mapping: NVXC's wire +/// values, which the guest's own constants hold, but which the `cli` feature alone, with which +/// this test runs, does not build. +const CWD_FAILED: &str = "cwd-failed"; +/// See [`CWD_FAILED`]. +const INVALID_REQUEST: &str = "invalid-request"; +/// See [`CWD_FAILED`]. +const MAPPINGS_INCOMPLETE: &str = "mappings-incomplete"; + /// An entry whose value has spaces, an equals sign, and a character beyond ASCII. const COMPLEX: &str = "COMPLEX=space = \u{2603}"; /// The file that a workload creates if it runs despite a refused working directory. @@ -390,12 +400,7 @@ fn layered_mapping_session( let expected = if accepted { (APP_READY, 900, 0, Vec::new()) } else { - ( - APP_ERROR, - 900, - 22, - protocol::INVALID_REQUEST.as_bytes().to_vec(), - ) + (APP_ERROR, 900, 22, INVALID_REQUEST.as_bytes().to_vec()) }; assert_eq!(session.read_response(Some(deadline)).unwrap(), expected); }; @@ -410,10 +415,7 @@ fn layered_mapping_session( None, WorkloadEnvironment::Default, ); - assert_eq!( - outcome.refusal, - Some((11, protocol::MAPPINGS_INCOMPLETE.to_owned())) - ); + assert_eq!(outcome.refusal, Some((11, MAPPINGS_INCOMPLETE.to_owned()))); assert!(outcome.exit.is_none()); }; incomplete(&mut session); diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index 0c2d03951..c4d2ce295 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -5,10 +5,11 @@ //! starts a managed agent, which runs workloads that the host sends it over an authenticated //! control console. This crate holds everything it takes to drive one: //! -//! - [`protocol`]: the codec of the control console, the `NVXS` session that OpenVMM brokers and -//! the `NVXC` frames of the guest agent, the guest targets that a sandbox's live shares may not -//! take, and the exit statuses with which a clone's guest fails fast. It has no dependencies, -//! so the host, the guest, and test doubles share it. +//! - [`protocol`]: the codec of the control console and the `NVXS` session that OpenVMM brokers, +//! which carries any application protocol, the guest targets that a sandbox's live shares may +//! not take, and the exit statuses with which a clone's guest fails fast. NVX's own control +//! protocol, `NVXC`, is private to the crate. It has no dependencies, so the host, the guest, +//! and test doubles share it. //! - [`activation`]: the activation record of a layered root, the bounded, versioned, non-secret //! record of the settings that its activation applies, which a clone restored from a template //! reads in place of its captured configuration, and what Ready reports about the activation. diff --git a/nvx-rs/src/protocol/app.rs b/nvx-rs/src/protocol/app.rs index fd8295aa3..da56b199e 100644 --- a/nvx-rs/src/protocol/app.rs +++ b/nvx-rs/src/protocol/app.rs @@ -5,12 +5,29 @@ //! request ID, a status, and the payload length) followed by the payload. Responses repeat the ID //! of their request. +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] use super::{ProtocolError, array, violation}; +#[cfg(any( + feature = "cli", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] use crate::activation::ActivationRecord; +#[cfg(any( + feature = "cli", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] use self::workload::WorkloadIdentity; /// Shared identity data; host-side selection remains in the host module. +#[cfg(any( + feature = "host", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] pub(crate) mod workload { /// The identity that workloads run as: a non-root user and group ID. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -30,100 +47,201 @@ pub(crate) mod workload { } /// Magic of every application frame. -pub const APP_MAGIC: [u8; 4] = *b"NVXC"; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_MAGIC: [u8; 4] = *b"NVXC"; /// Version of the application frame format. -pub const APP_VERSION: u8 = 1; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_VERSION: u8 = 1; /// Length of an application frame header. -pub const APP_HEADER_LEN: usize = 24; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_HEADER_LEN: usize = 24; /// Request that the guest agent acknowledge readiness with [`APP_READY`]. -pub const APP_PING: u8 = 1; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_PING: u8 = 1; /// Request that the guest agent run a workload; the payload is an `EXEC` payload. -pub const APP_EXEC: u8 = 2; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_EXEC: u8 = 2; /// Request that the guest agent shut the VM down, which it acknowledges with [`APP_STOPPED`]. -pub const APP_STOP: u8 = 3; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_STOP: u8 = 3; /// Request that the guest agent terminate the workload of the request with the frame's ID. -pub const APP_CANCEL: u8 = 4; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_CANCEL: u8 = 4; /// Request for the guest's control features, which the guest agent answers with [`APP_READY`] /// and a [`GuestFeatures`](super::GuestFeatures) payload. -pub const APP_FEATURES: u8 = 5; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_FEATURES: u8 = 5; /// Request that the guest agent mount entries of the host mapping table; the payload is a `MAPS` /// payload. -pub const APP_MAPS: u8 = 6; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_MAPS: u8 = 6; /// Request that nvx-init capture the VM after optionally killing its executions. -#[cfg_attr( - not(any( - test, - feature = "cli", - all(feature = "guest", target_os = "linux", target_arch = "x86_64") - )), - expect( - dead_code, - reason = "only the CLI's capture command sends it, and only nvx-init reads it" - ) -)] +#[cfg(any( + feature = "cli", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] pub(crate) const APP_CAPTURE: u8 = 7; /// Request that nvx-init activate a restored VM with its record and workload identity. -#[cfg_attr( - not(any( - test, - feature = "cli", - all(feature = "guest", target_os = "linux", target_arch = "x86_64") - )), - expect( - dead_code, - reason = "only the CLI's restore driver sends it, and only nvx-init reads it" - ) -)] +#[cfg(any( + feature = "cli", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] pub(crate) const APP_ACTIVATE: u8 = 8; /// Response that acknowledges a request. -pub const APP_READY: u8 = 0x81; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_READY: u8 = 0x81; /// Response that carries a chunk of a workload's standard output. -pub const APP_STDOUT: u8 = 0x82; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_STDOUT: u8 = 0x82; /// Response that carries a chunk of a workload's standard error. -pub const APP_STDERR: u8 = 0x83; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_STDERR: u8 = 0x83; /// Response that ends a workload: the status is its exit status, and the payload its /// [`ExitCategory`]. -pub const APP_EXIT: u8 = 0x84; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_EXIT: u8 = 0x84; /// Response that acknowledges [`APP_STOP`]. -pub const APP_STOPPED: u8 = 0x85; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_STOPPED: u8 = 0x85; /// Response that refuses a request: the status is an error number, and the payload an error /// category such as [`LAUNCH_FAILED`]. -pub const APP_ERROR: u8 = 0xff; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const APP_ERROR: u8 = 0xff; /// Error category with which a guest agent refuses a request kind that it does not know. -pub const UNSUPPORTED_OPERATION: &[u8] = b"unsupported-operation"; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const UNSUPPORTED_OPERATION: &[u8] = b"unsupported-operation"; /// Error category with which a guest agent refuses a malformed request. -pub const INVALID_REQUEST: &str = "invalid-request"; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const INVALID_REQUEST: &str = "invalid-request"; /// Error category with which a guest agent refuses a request while a workload runs. -pub const BUSY: &str = "busy"; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const BUSY: &str = "busy"; /// Error category with which a guest agent reports a workload that it could not launch. -pub const LAUNCH_FAILED: &str = "launch-failed"; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const LAUNCH_FAILED: &str = "launch-failed"; /// Error category with which a guest agent reports a working directory that the workload cannot /// enter. The status is the guest's error number, and nothing of the workload ran. -pub const CWD_FAILED: &str = "cwd-failed"; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const CWD_FAILED: &str = "cwd-failed"; /// Error category with which a guest agent refuses a workload until it has applied every host /// mapping that the kernel command line announced. -pub const MAPPINGS_INCOMPLETE: &str = "mappings-incomplete"; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const MAPPINGS_INCOMPLETE: &str = "mappings-incomplete"; /// Error category with which a guest agent reports a host mapping that it could not apply. The /// status is the guest's error number. -pub const MAPPING_FAILED: &str = "mapping-failed"; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) const MAPPING_FAILED: &str = "mapping-failed"; /// Decoded application frame. +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AppFrame<'a> { +pub(crate) struct AppFrame<'a> { /// One of the `APP_*` frame kinds. - pub kind: u8, + pub(crate) kind: u8, /// ID of the request, which its responses repeat. - pub request_id: u64, + pub(crate) request_id: u64, /// Status of a response; zero in requests. - pub status: i32, + pub(crate) status: i32, /// Payload that follows the header. - pub payload: &'a [u8], + pub(crate) payload: &'a [u8], } /// Encodes one application frame. -pub fn encode_app(kind: u8, request_id: u64, status: i32, payload: &[u8]) -> Vec { +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) fn encode_app(kind: u8, request_id: u64, status: i32, payload: &[u8]) -> Vec { let length = u32::try_from(payload.len()).unwrap_or(u32::MAX); let mut frame = Vec::with_capacity(APP_HEADER_LEN + payload.len()); frame.extend_from_slice(&APP_MAGIC); @@ -138,7 +256,12 @@ pub fn encode_app(kind: u8, request_id: u64, status: i32, payload: &[u8]) -> Vec } /// Decodes and validates an application frame. -pub fn decode_app(frame: &[u8]) -> Result, ProtocolError> { +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) fn decode_app(frame: &[u8]) -> Result, ProtocolError> { if frame.len() < APP_HEADER_LEN { return Err(violation( "control endpoint returned a truncated application frame", @@ -161,25 +284,13 @@ pub fn decode_app(frame: &[u8]) -> Result, ProtocolError> { } /// Encodes the capture request's flag as one byte, 0 or 1. -#[cfg_attr( - not(any(test, feature = "cli")), - expect( - dead_code, - reason = "only the CLI's capture command sends capture requests" - ) -)] +#[cfg(feature = "cli")] pub(crate) fn encode_capture_payload(kill_executions: bool) -> [u8; 1] { [u8::from(kill_executions)] } /// Decodes the capture request's sole kill-executions flag. -#[cfg_attr( - not(any( - test, - all(feature = "guest", target_os = "linux", target_arch = "x86_64") - )), - expect(dead_code, reason = "SL7.7a makes nvx-init handle capture requests") -)] +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] pub(crate) fn decode_capture_payload(payload: &[u8]) -> Result { match payload { [0] => Ok(false), @@ -194,13 +305,7 @@ pub(crate) fn decode_capture_payload(payload: &[u8]) -> Result Result<(ActivationRecord, WorkloadIdentity), ProtocolError> { @@ -249,6 +348,10 @@ pub(crate) fn decode_activate_payload( } /// Matches the guest's non-root credential range, excluding the unchanged-ID sentinel. +#[cfg(any( + feature = "cli", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn check_workload_identity(workload: &WorkloadIdentity) -> Result<(), ProtocolError> { if matches!(workload.uid, 0 | u32::MAX) || matches!(workload.gid, 0 | u32::MAX) { return Err(violation( @@ -259,8 +362,9 @@ fn check_workload_identity(workload: &WorkloadIdentity) -> Result<(), ProtocolEr } /// Exit category reported by the guest agent, the payload of an [`APP_EXIT`] response. +#[cfg(any(feature = "cli", feature = "testing"))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExitCategory { +pub(crate) enum ExitCategory { /// The workload exited; the status is its exit code. Exit, /// The workload's timeout elapsed and the guest agent killed it. @@ -275,9 +379,11 @@ pub enum ExitCategory { Cancelled, } +#[cfg(any(feature = "cli", feature = "testing"))] impl ExitCategory { /// Parses the payload of an [`APP_EXIT`] response. - pub fn parse(payload: &[u8]) -> Result { + #[cfg(feature = "cli")] + pub(crate) fn parse(payload: &[u8]) -> Result { match payload { b"exit" => Ok(Self::Exit), b"timeout" => Ok(Self::Timeout), @@ -292,7 +398,8 @@ impl ExitCategory { } /// Returns the payload of an [`APP_EXIT`] response that reports this category. - pub const fn as_str(self) -> &'static str { + #[cfg(feature = "testing")] + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Exit => "exit", Self::Timeout => "timeout", diff --git a/nvx-rs/src/protocol/exec.rs b/nvx-rs/src/protocol/exec.rs index 20937d0de..2795f2cf9 100644 --- a/nvx-rs/src/protocol/exec.rs +++ b/nvx-rs/src/protocol/exec.rs @@ -10,38 +10,41 @@ use std::collections::BTreeSet; -use super::{APP_HEADER_LEN, OUTER_MAX_PAYLOAD, ProtocolError, u16_at, u32_at, violation}; +#[cfg(feature = "cli")] +use super::{APP_HEADER_LEN, OUTER_MAX_PAYLOAD}; +use super::{ProtocolError, violation}; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +use super::{u16_at, u32_at}; /// Largest number of workload arguments, including the program. -pub const MAX_ARGUMENTS: usize = 64; +pub(crate) const MAX_ARGUMENTS: usize = 64; /// Largest encoded size of one workload argument, working directory, or environment entry. -pub const MAX_ARGUMENT_BYTES: usize = 4096; -/// Largest working directory that hosts send: Linux's `PATH_MAX` without the terminating NUL, one -/// byte below the agent's bound. -pub const MAX_CWD_BYTES: usize = 4095; +pub(crate) const MAX_ARGUMENT_BYTES: usize = 4096; /// Largest working directory that the C guest decoder accepts. Unlike host paths, the guest's /// wire bound includes all 4096 non-NUL bytes because its terminating NUL is allocated separately. -pub const MAX_GUEST_CWD_BYTES: usize = MAX_ARGUMENT_BYTES; +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] +pub(crate) const MAX_GUEST_CWD_BYTES: usize = MAX_ARGUMENT_BYTES; /// Largest number of workload environment entries. -pub const MAX_ENVIRONMENT: usize = 256; -/// Largest workload timeout the guest agent accepts: any value of the exec request's 32-bit -/// timeout field, which is also the largest `process.timeout` that MXC's schema allows. -pub const MAX_TIMEOUT_MS: u32 = u32::MAX; +pub(crate) const MAX_ENVIRONMENT: usize = 256; /// Largest combined stdout and stderr volume the guest agent forwards for one execution. -pub const MAX_OUTPUT_BYTES: usize = 1024 * 1024; +pub(crate) const MAX_OUTPUT_BYTES: usize = 1024 * 1024; /// Extension field of a payload that carries flags, a working directory, and an environment. -pub const EXEC_EXTENDED: u16 = 1; +pub(crate) const EXEC_EXTENDED: u16 = 1; /// Flag of an extended payload that carries a working directory. -pub const EXEC_CWD_PRESENT: u16 = 1 << 0; +pub(crate) const EXEC_CWD_PRESENT: u16 = 1 << 0; /// Flag of an extended payload that carries an explicit environment. -pub const EXEC_ENVIRONMENT_PRESENT: u16 = 1 << 1; +pub(crate) const EXEC_ENVIRONMENT_PRESENT: u16 = 1 << 1; /// Flag of an extended payload whose environment entries layer over the default environment. -pub const EXEC_INHERIT_DEFAULT_ENV: u16 = 1 << 2; +pub(crate) const EXEC_INHERIT_DEFAULT_ENV: u16 = 1 << 2; /// The environment that the guest agent gives a workload. +#[cfg(feature = "cli")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WorkloadEnvironment<'a> { +pub(crate) enum WorkloadEnvironment<'a> { /// The guest's default environment. Default, /// Exactly these `KEY=VALUE` entries; an empty slice is an empty environment. @@ -52,17 +55,18 @@ pub enum WorkloadEnvironment<'a> { } /// A workload that an `EXEC` request asks the guest agent to run. +#[cfg(feature = "cli")] #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Workload<'a> { +pub(crate) struct Workload<'a> { /// The program, an absolute guest path, followed by its arguments. - pub argv: Vec, + pub(crate) argv: Vec, /// Timeout in milliseconds; zero disables it. - pub timeout_ms: u32, + pub(crate) timeout_ms: u32, /// Absolute guest directory that the guest agent enters before it starts the program; /// `None` selects `/`. - pub cwd: Option<&'a str>, + pub(crate) cwd: Option<&'a str>, /// The program's environment. - pub environment: WorkloadEnvironment<'a>, + pub(crate) environment: WorkloadEnvironment<'a>, } /// Encodes the payload of an `EXEC` request. @@ -70,10 +74,11 @@ pub struct Workload<'a> { /// The guest agent requires 1 to [`MAX_ARGUMENTS`] non-empty arguments without NUL bytes, each /// at most [`MAX_ARGUMENT_BYTES`] long, an absolute program path, an absolute working directory /// of at most [`MAX_ARGUMENT_BYTES`] without NUL bytes, and at most [`MAX_ENVIRONMENT`] -/// `KEY=VALUE` environment entries with unique names. It accepts any timeout up to -/// [`MAX_TIMEOUT_MS`], and zero disables it. A workload without a working directory or an -/// explicit environment keeps the legacy payload. -pub fn encode_exec_payload(workload: &Workload<'_>) -> Result, ProtocolError> { +/// `KEY=VALUE` environment entries with unique names. It accepts any value of the 32-bit +/// timeout, and zero disables it. A workload without a working directory or an explicit +/// environment keeps the legacy payload. +#[cfg(feature = "cli")] +pub(crate) fn encode_exec_payload(workload: &Workload<'_>) -> Result, ProtocolError> { let argv = &workload.argv; if argv.is_empty() || argv.len() > MAX_ARGUMENTS { return Err(violation(format!( @@ -164,19 +169,20 @@ pub fn encode_exec_payload(workload: &Workload<'_>) -> Result, ProtocolE } /// An `EXEC` payload as the guest agent reads it. +#[cfg(feature = "testing")] #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExecPayload { +pub(crate) struct ExecPayload { /// Timeout in milliseconds; zero disables it. - pub timeout_ms: u32, + pub(crate) timeout_ms: u32, /// The program, followed by its arguments. - pub argv: Vec, + pub(crate) argv: Vec, /// Absolute guest directory to start the program in; `None` selects `/`. - pub cwd: Option, + pub(crate) cwd: Option, /// The explicit `KEY=VALUE` environment entries, or `None` for the default environment. - pub environment: Option>, + pub(crate) environment: Option>, /// Whether the program starts from the default environment: true without explicit entries, /// and true when the entries layer over the default environment rather than replace it. - pub inherit_default_env: bool, + pub(crate) inherit_default_env: bool, } /// An `EXEC` payload as the guest agent reads it, without imposing UTF-8 on guest byte strings. @@ -184,21 +190,26 @@ pub struct ExecPayload { /// Linux arguments, paths, and environment entries are byte strings. Keeping this form separate /// from [`ExecPayload`] preserves the host and fake-OpenVMM APIs, which intentionally use UTF-8, /// while accepting every non-NUL byte sequence that the C guest accepted. +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] #[derive(Debug, Clone, PartialEq, Eq)] -pub struct GuestExecPayload { +pub(crate) struct GuestExecPayload { /// Timeout in milliseconds; zero disables it. - pub timeout_ms: u32, + pub(crate) timeout_ms: u32, /// The absolute program, followed by its arguments. - pub argv: Vec>, + pub(crate) argv: Vec>, /// Absolute guest directory to start the program in; `None` selects `/`. - pub cwd: Option>, + pub(crate) cwd: Option>, /// Explicit `KEY=VALUE` entries, or `None` for the default environment. - pub environment: Option>>, + pub(crate) environment: Option>>, /// Whether explicit entries layer over the default environment. - pub inherit_default_env: bool, + pub(crate) inherit_default_env: bool, } /// The fixed fields of an `EXEC` payload, which both decoders read and check alike. +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] struct ExecHeader { /// Timeout in milliseconds; zero disables it. timeout_ms: u32, @@ -221,6 +232,10 @@ struct ExecHeader { /// unknown extension or flag, a working-directory length without the working-directory flag or /// the reverse, environment entries without the environment flag, and more than /// [`MAX_ENVIRONMENT`] entries. +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn decode_exec_header(payload: &[u8], max_cwd: usize) -> Result { let truncated = || violation("the exec request is truncated"); if payload.len() < 8 { @@ -273,6 +288,10 @@ fn decode_exec_header(payload: &[u8], max_cwd: usize) -> Result { /// Timeout in milliseconds; zero disables it. timeout_ms: u32, @@ -294,6 +313,10 @@ struct DecodedExec { /// entry, or one longer than [`MAX_ARGUMENT_BYTES`], one that contains NUL characters or that `S` /// refuses, a relative program or working directory, an entry without a name, a name that /// repeats, and trailing bytes. +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn decode_exec( payload: &[u8], max_cwd: usize, @@ -368,7 +391,8 @@ fn decode_exec( /// longer than [`MAX_ARGUMENT_BYTES`], text that is not UTF-8 or contains NUL characters, a /// relative program or working directory, an entry without a name, a name that repeats, and /// trailing bytes. -pub fn decode_exec_payload(payload: &[u8]) -> Result { +#[cfg(feature = "testing")] +pub(crate) fn decode_exec_payload(payload: &[u8]) -> Result { let DecodedExec { timeout_ms, flags, @@ -395,7 +419,8 @@ pub fn decode_exec_payload(payload: &[u8]) -> Result /// [`EXEC_INHERIT_DEFAULT_ENV`] alone, so unlike the host's, it is false without explicit entries. /// The host-facing decoder remains text-oriented, so this guest decoder does not broaden host /// behavior. -pub fn decode_guest_exec_payload(payload: &[u8]) -> Result { +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] +pub(crate) fn decode_guest_exec_payload(payload: &[u8]) -> Result { let DecodedExec { timeout_ms, flags, @@ -416,6 +441,10 @@ pub fn decode_guest_exec_payload(payload: &[u8]) -> Result + Sized { /// Converts the bytes of a string of the payload, which `what` names, as soon as they are /// read. @@ -429,6 +458,7 @@ trait ExecString: AsRef<[u8]> + Sized { } /// The host's decoder refuses bytes that are not UTF-8, and names a variable that repeats. +#[cfg(feature = "testing")] impl ExecString for String { fn convert(bytes: Vec, what: &str) -> Result { String::from_utf8(bytes).map_err(|_| violation(format!("exec {what} must be valid UTF-8"))) @@ -448,6 +478,7 @@ impl ExecString for String { } /// The guest's decoder keeps Linux byte strings as they are. +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] impl ExecString for Vec { fn convert(bytes: Vec, _what: &str) -> Result { Ok(bytes) @@ -464,6 +495,10 @@ impl ExecString for Vec { /// Reads the string at `offset` that its `u32` length precedes, of 1 to [`MAX_ARGUMENT_BYTES`] /// bytes, as `S`, and advances `offset` past it. +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn prefixed_string( payload: &[u8], offset: &mut usize, @@ -473,6 +508,10 @@ fn prefixed_string( } /// Reads one little-endian length-prefixed byte string and advances `offset`. +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn prefixed_bytes( payload: &[u8], offset: &mut usize, @@ -496,6 +535,10 @@ fn prefixed_bytes( } /// Returns one bounded non-NUL byte range without imposing UTF-8. +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn bytes_at<'a>( payload: &'a [u8], offset: usize, diff --git a/nvx-rs/src/protocol/features.rs b/nvx-rs/src/protocol/features.rs index 9789c9c5a..a303a5da4 100644 --- a/nvx-rs/src/protocol/features.rs +++ b/nvx-rs/src/protocol/features.rs @@ -2,43 +2,47 @@ use std::ops::BitOr; +#[cfg(feature = "cli")] use super::{ProtocolError, array, violation}; -/// Control features of a guest image, advertised in the response to -/// [`APP_FEATURES`](super::APP_FEATURES). +/// Control features of a guest image, advertised in the response to `APP_FEATURES`. /// /// The Rust guest agent uses these shared definitions and also speaks for the features of its -/// image's init script. A host drives a sandbox only when its guest provides every feature in -/// [`GuestFeatures::REQUIRED`]; an older image would silently ignore the policy or the request -/// that depends on the missing feature. +/// image's init script. A host drives a sandbox only when its guest provides every feature that +/// the host depends on, such as [`LAYERED_SANDBOX`](Self::LAYERED_SANDBOX) for `nvx-rs sandbox`; +/// an older image would silently ignore the policy or the request that depends on the missing +/// feature. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct GuestFeatures(u32); +pub(crate) struct GuestFeatures(u32); impl GuestFeatures { /// What a guest agent that predates the `APP_FEATURES` request provides. - pub const NONE: Self = Self(0); + #[cfg(feature = "cli")] + pub(crate) const NONE: Self = Self(0); /// Terminates a workload on `APP_CANCEL` and reports the `cancelled` exit category. - pub const CANCEL: Self = Self(1 << 0); + pub(crate) const CANCEL: Self = Self(1 << 0); // Bit 1 announced the bind mounts that `nvx_map=` kernel command-line tokens listed. Guests // no longer set it, so a host that still sends those tokens refuses them. /// Creates the account that `nvx_workload_account=create` selects. Only the blockless root /// creates accounts; the layered root's executions require an existing one. - pub const WORKLOAD_ACCOUNT: Self = Self(1 << 2); + pub(crate) const WORKLOAD_ACCOUNT: Self = Self(1 << 2); /// Runs each workload in a cgroup of its own and kills whatever it leaves behind. - pub const EXEC_CGROUP: Self = Self(1 << 3); + #[cfg(feature = "cli")] + pub(crate) const EXEC_CGROUP: Self = Self(1 << 3); /// Applies the working directory and the explicit environment of an extended `EXEC` request /// to that execution alone, and layers the environment over the default one on request. - pub const EXEC_ENVIRONMENT: Self = Self(1 << 4); + pub(crate) const EXEC_ENVIRONMENT: Self = Self(1 << 4); /// Starts each workload in the working directory that its `EXEC` request names, or in `/`, /// entered with the workload's identity, and refuses the launch with the `cwd-failed` /// category when the workload cannot enter it. - pub const EXEC_CWD: Self = Self(1 << 5); + pub(crate) const EXEC_CWD: Self = Self(1 << 5); /// Bind-mounts the host mapping table of `APP_MAPS` requests, as many entries as the /// `nvx_maps=` kernel command-line token announces, keeps the shared export root-only, and /// refuses workloads until every entry is mounted. - pub const HOST_MAPPING_TABLE: Self = Self(1 << 6); + pub(crate) const HOST_MAPPING_TABLE: Self = Self(1 << 6); /// Features announced by the direct-mode managed agent. - pub const DIRECT_AGENT: Self = Self( + #[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] + pub(crate) const DIRECT_AGENT: Self = Self( Self::CANCEL.0 | Self::WORKLOAD_ACCOUNT.0 | Self::EXEC_ENVIRONMENT.0 @@ -46,10 +50,12 @@ impl GuestFeatures { | Self::HOST_MAPPING_TABLE.0, ); /// Features announced by the managed agent that launches a sandbox container. - pub const SANDBOX_AGENT: Self = + #[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") + ))] + pub(crate) const SANDBOX_AGENT: Self = Self(Self::CANCEL.0 | Self::EXEC_ENVIRONMENT.0 | Self::HOST_MAPPING_TABLE.0); - /// The features that the host depends on. - pub const REQUIRED: Self = Self::DIRECT_AGENT; /// The features that `nvx-rs sandbox` depends on in the layered sandbox root, whose agent /// announces [`SANDBOX_AGENT`](Self::SANDBOX_AGENT): /// [`EXEC_ENVIRONMENT`](Self::EXEC_ENVIRONMENT), because `exec` sends a working directory and @@ -62,9 +68,12 @@ impl GuestFeatures { /// when OpenVMM resets the session that it ran for. It holds no /// [`WORKLOAD_ACCOUNT`](Self::WORKLOAD_ACCOUNT), because no sandbox launch requests account /// creation, which the layered root neither performs nor announces (microsoft/nvx#566). - pub const LAYERED_SANDBOX: Self = Self(Self::EXEC_ENVIRONMENT.0 | Self::HOST_MAPPING_TABLE.0); + #[cfg(feature = "cli")] + pub(crate) const LAYERED_SANDBOX: Self = + Self(Self::EXEC_ENVIRONMENT.0 | Self::HOST_MAPPING_TABLE.0); /// Human-readable names of every required feature, used in compatibility errors. + #[cfg(feature = "cli")] pub(crate) const NAMES: [(Self, &'static str); 6] = [ (Self::CANCEL, "cancellation"), (Self::WORKLOAD_ACCOUNT, "workload accounts"), @@ -75,24 +84,22 @@ impl GuestFeatures { ]; /// Returns the features whose bits are set in `bits`. - pub const fn from_bits(bits: u32) -> Self { + #[cfg(feature = "testing")] + pub(crate) const fn from_bits(bits: u32) -> Self { Self(bits) } /// Returns the bits of these features. - pub const fn bits(self) -> u32 { + #[cfg(feature = "testing")] + pub(crate) const fn bits(self) -> u32 { self.0 } - /// Returns whether this set holds every feature of `other`. - pub const fn contains(self, other: Self) -> bool { - self.0 & other.0 == other.0 - } - /// Decodes the payload of a features response. /// /// Later revisions may append fields, so trailing bytes are ignored. - pub fn decode(payload: &[u8]) -> Result { + #[cfg(feature = "cli")] + pub(crate) fn decode(payload: &[u8]) -> Result { let bits = payload .get(..4) .ok_or_else(|| violation("the guest agent returned a truncated features response"))?; @@ -100,12 +107,17 @@ impl GuestFeatures { } /// Encodes the payload of a features response, as a guest agent sends it. - pub const fn encode(self) -> [u8; 4] { + #[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") + ))] + pub(crate) const fn encode(self) -> [u8; 4] { self.0.to_le_bytes() } /// Names the features of `required` that this set lacks. - pub fn missing(self, required: Self) -> Vec<&'static str> { + #[cfg(feature = "cli")] + pub(crate) fn missing(self, required: Self) -> Vec<&'static str> { Self::NAMES .iter() .filter(|(feature, _)| required.0 & feature.0 != 0 && self.0 & feature.0 == 0) diff --git a/nvx-rs/src/protocol/maps.rs b/nvx-rs/src/protocol/maps.rs index f72d4353f..4997087d7 100644 --- a/nvx-rs/src/protocol/maps.rs +++ b/nvx-rs/src/protocol/maps.rs @@ -1,5 +1,5 @@ -//! `MAPS` payloads: the host mapping table that [`APP_MAPS`](super::APP_MAPS) requests hand the -//! guest agent. +//! `MAPS` payloads: the host mapping table that the `APP_MAPS` requests of NVX's own control +//! protocol, `NVXC`, hand the guest agent. //! //! A payload holds the index of its first entry (`u32`) and its number of entries (`u32`), //! followed by the entries. Each entry holds its flags (`u16`), the lengths of its source and diff --git a/nvx-rs/src/protocol/mod.rs b/nvx-rs/src/protocol/mod.rs index 9d5dc173e..a89f0ffe2 100644 --- a/nvx-rs/src/protocol/mod.rs +++ b/nvx-rs/src/protocol/mod.rs @@ -1,18 +1,14 @@ //! Codec of the OpenVMM control-console protocol. //! //! OpenVMM brokers an authenticated stream of outer records (magic `NVXS`) between one host -//! client and the Rust guest agent. Data records carry application frames (magic `NVXC`) that -//! the host and guest share from this module. All integers are little-endian. +//! client and the guest's agent. Data records carry the bytes of an application protocol. NVX's +//! own, `NVXC`, with its frames, its `EXEC` payloads and the control features that a guest image +//! advertises, is private to this crate: only NVX's programs speak it, the CLI, nvx-init, the +//! managed agent and the fake OpenVMM. All integers are little-endian. //! //! - Outer records: [`encode_outer`] and [`decode_outer_header`], and the `OUTER_*` record types. -//! - Application frames: [`encode_app`] and [`decode_app`], the `APP_*` request and response -//! kinds, the error categories with which the guest agent refuses a request, and the -//! [`ExitCategory`] of a workload. -//! - `EXEC` payloads: the [`Workload`] that the host encodes with [`encode_exec_payload`], and -//! the [`ExecPayload`] that the guest agent decodes with [`decode_exec_payload`]. //! - `MAPS` payloads: the [`HostMap`] table that the host encodes with [`encode_maps_payloads`], //! and the [`MapsPayload`] that the guest agent decodes with [`decode_maps_payload`]. -//! - [`GuestFeatures`]: the control features that a guest image advertises. //! - The kernel command-line tokens through which the host configures the guest agent at boot, //! such as [`MAPS_TOKEN`]. //! - [`cmdline`]: the kernel command line as Linux reads it, for the readers that need the @@ -42,8 +38,18 @@ use std::fmt; mod app; mod boot; pub mod cmdline; -mod exec; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) mod exec; pub mod export; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] mod features; pub mod maps; pub mod nvxs; @@ -66,6 +72,15 @@ pub(crate) use app::APP_ACTIVATE; all(feature = "guest", target_os = "linux", target_arch = "x86_64") ))] pub(crate) use app::APP_CAPTURE; +#[cfg(any( + feature = "cli", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) use app::APP_HEADER_LEN; +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] +pub(crate) use app::AppFrame; +#[cfg(any(feature = "cli", feature = "testing"))] +pub(crate) use app::ExitCategory; #[cfg(feature = "cli")] pub(crate) use app::encode_activate_payload; #[cfg(feature = "cli")] @@ -75,22 +90,42 @@ pub(crate) use app::encode_capture_payload; all(feature = "guest", target_os = "linux", target_arch = "x86_64") ))] pub(crate) use app::workload; -pub use app::{ - APP_CANCEL, APP_ERROR, APP_EXEC, APP_EXIT, APP_FEATURES, APP_HEADER_LEN, APP_MAGIC, APP_MAPS, - APP_PING, APP_READY, APP_STDERR, APP_STDOUT, APP_STOP, APP_STOPPED, APP_VERSION, AppFrame, - BUSY, CWD_FAILED, ExitCategory, INVALID_REQUEST, LAUNCH_FAILED, MAPPING_FAILED, - MAPPINGS_INCOMPLETE, UNSUPPORTED_OPERATION, decode_app, encode_app, +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) use app::{ + APP_CANCEL, APP_ERROR, APP_EXEC, APP_EXIT, APP_FEATURES, APP_MAPS, APP_PING, APP_READY, + APP_STDERR, APP_STDOUT, APP_STOP, APP_STOPPED, MAPPING_FAILED, UNSUPPORTED_OPERATION, + decode_app, encode_app, }; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) use app::{BUSY, CWD_FAILED, INVALID_REQUEST, LAUNCH_FAILED, MAPPINGS_INCOMPLETE}; #[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] pub(crate) use app::{decode_activate_payload, decode_capture_payload}; pub use boot::{MAPS_TOKEN, WORKLOAD_ACCOUNT_TOKEN, parse_host_mapping_count}; -pub use exec::{ - EXEC_CWD_PRESENT, EXEC_ENVIRONMENT_PRESENT, EXEC_EXTENDED, EXEC_INHERIT_DEFAULT_ENV, - ExecPayload, GuestExecPayload, MAX_ARGUMENT_BYTES, MAX_ARGUMENTS, MAX_CWD_BYTES, - MAX_ENVIRONMENT, MAX_GUEST_CWD_BYTES, MAX_OUTPUT_BYTES, MAX_TIMEOUT_MS, Workload, - WorkloadEnvironment, decode_exec_payload, decode_guest_exec_payload, encode_exec_payload, -}; -pub use features::GuestFeatures; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) use exec::MAX_OUTPUT_BYTES; +#[cfg(feature = "testing")] +pub(crate) use exec::decode_exec_payload; +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] +pub(crate) use exec::{GuestExecPayload, decode_guest_exec_payload}; +#[cfg(feature = "cli")] +pub(crate) use exec::{Workload, WorkloadEnvironment, encode_exec_payload}; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) use features::GuestFeatures; pub use maps::{ GuestHostMap, GuestMapsPayload, HostMap, MAP_ENTRY_HEADER_LEN, MAP_READ_ONLY, MAPS_HEADER_LEN, MAX_HOST_MAPPINGS, MAX_MAP_PATH_BYTES, MapsPayload, decode_guest_maps_payload, diff --git a/nvx-rs/src/protocol/tests.rs b/nvx-rs/src/protocol/tests.rs index 72ab4809b..215eba5f5 100644 --- a/nvx-rs/src/protocol/tests.rs +++ b/nvx-rs/src/protocol/tests.rs @@ -5,14 +5,120 @@ //! encoders, and the decoders that the guest agent and the fake OpenVMM share with it, must all //! match them, so a change to the codec that every side would agree on still fails here. +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] use proptest::prelude::*; use super::nvxs::{RecordParser, RecordType, encode_record}; use super::*; +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] use crate::activation::ActivationRecord; -use crate::tests::{ACTIVATION_V2, NVXC_V1, Values, of_kind, vector, vectors}; +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] +use crate::tests::ACTIVATION_V2; +#[cfg(all( + feature = "cli", + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] +use crate::tests::Values; +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] +use crate::tests::vectors; +use crate::tests::{NVXC_V1, of_kind, vector}; + +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +use super::app::{APP_HEADER_LEN, APP_VERSION, AppFrame}; +#[cfg(all( + feature = "cli", + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] +use super::exec::ExecPayload; +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] +use super::exec::MAX_GUEST_CWD_BYTES; +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +use super::exec::{ + EXEC_CWD_PRESENT, EXEC_ENVIRONMENT_PRESENT, EXEC_EXTENDED, EXEC_INHERIT_DEFAULT_ENV, + MAX_ARGUMENTS, +}; +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +use super::exec::{MAX_ARGUMENT_BYTES, MAX_ENVIRONMENT}; + +/// Largest working directory that hosts send: Linux's `PATH_MAX` without the terminating NUL, one +/// byte below the agent's bound. Only these tests read it. +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +const MAX_CWD_BYTES: usize = 4095; +/// Largest workload timeout the guest agent accepts: any value of the exec request's 32-bit +/// timeout field, which is also the largest `process.timeout` that MXC's schema allows. Only these +/// tests read it. +#[cfg(feature = "cli")] +const MAX_TIMEOUT_MS: u32 = u32::MAX; + +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +impl GuestFeatures { + /// The features that the host once required, which the direct-mode managed agent announces. + /// Only tests read them. + #[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" + ))] + pub(crate) const REQUIRED: Self = Self::DIRECT_AGENT; + + /// Returns whether this set holds every feature of `other`. + #[cfg(any( + all(feature = "cli", feature = "testing"), + all( + any(feature = "cli", feature = "testing"), + feature = "guest", + target_os = "linux", + target_arch = "x86_64" + ) + ))] + pub(crate) fn contains(self, other: Self) -> bool { + (self | other) == self + } +} /// A workload that runs `argv` without a timeout in `/` with the default environment. +#[cfg(feature = "cli")] fn program<'a>(argv: &[&str]) -> Workload<'a> { Workload { argv: argv.iter().map(|argument| (*argument).to_owned()).collect(), @@ -22,6 +128,7 @@ fn program<'a>(argv: &[&str]) -> Workload<'a> { } } +#[cfg(feature = "cli")] fn strings(values: &[&str]) -> Vec { values.iter().map(|value| (*value).to_owned()).collect() } @@ -35,6 +142,11 @@ fn record_type(name: &str) -> RecordType { } /// Returns the frame kind that a vector names. +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] fn frame_kind(name: &str) -> u8 { match name { "PING" => APP_PING, @@ -135,6 +247,11 @@ fn records_match_their_vectors() { } /// Each `FRAME` vector decodes to its values, which encode to its bytes. +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn frames_match_their_vectors() { let names = ["PING", "STDOUT", "EXIT", "ERROR_CWD_FAILED"]; @@ -158,6 +275,12 @@ fn frames_match_their_vectors() { } } +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn capture_requests_round_trip_both_kill_flags() { for (payload, kill_executions) in [([0], false), ([1], true)] { @@ -172,6 +295,7 @@ fn capture_requests_round_trip_both_kill_flags() { } } +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] #[test] fn capture_requests_refuse_unknown_or_mis_sized_flags() { for payload in [&[][..], &[2], &[1, 0]] { @@ -179,6 +303,12 @@ fn capture_requests_refuse_unknown_or_mis_sized_flags() { } } +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn activate_requests_round_trip_records_and_workload_identity() { let names = [ @@ -210,6 +340,7 @@ fn activate_requests_round_trip_records_and_workload_identity() { } } +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] #[test] fn activate_requests_refuse_truncated_or_malformed_payloads() { let record = vector(ACTIVATION_V2, "RECORD", "GPT_DIRECTORY").bytes; @@ -233,6 +364,12 @@ fn activate_requests_refuse_truncated_or_malformed_payloads() { assert!(app::decode_activate_payload(&trailing).is_err()); } +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn activate_workload_ids_match_guest_credential_bounds() { let record = ActivationRecord::decode(&vector(ACTIVATION_V2, "RECORD", "CLONE").bytes).unwrap(); @@ -268,6 +405,13 @@ fn activate_workload_ids_match_guest_credential_bounds() { /// the host encodes to its bytes. Without an explicit environment, the host's /// `inherit_default_env` is true, and the guest's, which follows the layering flag alone, false /// (microsoft/nvx#581). +#[cfg(all( + feature = "cli", + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn exec_payloads_match_their_vectors() { let names = [ @@ -382,6 +526,7 @@ fn maps_payloads_match_their_vectors() { } /// Each `FEATURES` vector decodes to its bits, which encode to its bytes. +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn features_match_their_vectors() { for vector in of_kind(NVXC_V1, "FEATURES", &["ALL"]) { @@ -394,6 +539,12 @@ fn features_match_their_vectors() { } } +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn the_host_requires_every_feature() { assert_eq!( @@ -408,6 +559,7 @@ fn the_host_requires_every_feature() { assert!(!GuestFeatures::REQUIRED.contains(GuestFeatures::EXEC_CGROUP)); } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn exec_payload_carries_every_32_bit_timeout() { for timeout_ms in [0, 3_600_001, 86_400_000, MAX_TIMEOUT_MS] { @@ -425,6 +577,7 @@ fn exec_payload_carries_every_32_bit_timeout() { } } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn exec_payload_carries_the_working_directory_natively() { for cwd in ["", "tmp", &format!("/{}", "a".repeat(4096)), "/t\0mp"] { @@ -443,6 +596,7 @@ fn exec_payload_carries_the_working_directory_natively() { assert_eq!(decode_exec_payload(&payload).unwrap().cwd, Some(longest)); } +#[cfg(feature = "cli")] #[test] fn exec_payload_distinguishes_explicit_environments() { let environment = strings(&["FOO=a b", "EMPTY="]); @@ -462,6 +616,7 @@ fn exec_payload_distinguishes_explicit_environments() { assert_eq!(&default[6..8], &[0, 0]); } +#[cfg(feature = "cli")] #[test] fn exec_payload_enforces_agent_limits() { let encode = |argv: Vec, timeout_ms| { @@ -514,6 +669,7 @@ fn exec_payload_enforces_agent_limits() { } /// Encodes an extended exec request with an explicit environment, without the host's checks. +#[cfg(all(feature = "cli", feature = "testing"))] fn extended(argv: &[&str], cwd: Option<&str>, environment: &[&str]) -> Vec { let length = |text: &str| u32::try_from(text.len()).unwrap().to_le_bytes(); let mut flags = EXEC_ENVIRONMENT_PRESENT; @@ -538,6 +694,7 @@ fn extended(argv: &[&str], cwd: Option<&str>, environment: &[&str]) -> Vec { payload } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn exec_requests_are_refused_where_the_guest_agent_refuses_them() { let exec = decode_exec_payload(&extended( @@ -599,6 +756,11 @@ fn exec_requests_are_refused_where_the_guest_agent_refuses_them() { assert_eq!(most.argv.len(), MAX_ARGUMENTS); } +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn decoders_reject_malformed_input() { let mut header = [0u8; OUTER_HEADER_LEN]; @@ -615,6 +777,7 @@ fn decoders_reject_malformed_input() { assert!(decode_app(&frame[..10]).is_err()); } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn exit_categories_match_the_guest_agent() { for category in [ @@ -654,6 +817,13 @@ fn exit_categories_match_the_guest_agent() { assert!(ExitCategory::parse(b"canceled").is_err()); } +#[cfg(all( + feature = "cli", + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn guest_features_report_what_a_guest_lacks() { let required = GuestFeatures::REQUIRED; @@ -688,6 +858,7 @@ fn guest_features_report_what_a_guest_lacks() { ); } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn guest_features_use_distinct_bits() { let mut seen = 1u32 << 1; @@ -706,6 +877,11 @@ fn map(source: &str, target: &str, read_only: bool) -> HostMap { } } +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn maps_payloads_are_split_to_fit_the_limit() { let maps: Vec = (0..10) @@ -777,6 +953,11 @@ fn maps_payloads_are_split_to_fit_the_limit() { ); } +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn maps_payloads_enforce_agent_limits() { let limit = OUTER_MAX_PAYLOAD - APP_HEADER_LEN; @@ -864,6 +1045,11 @@ fn maps_decoders_refuse_the_invalid_vectors() { // Hosts and guests share the wire constants, so changing one passes every round trip between // them yet breaks compatibility with released peers. These tests pin each value literally. +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn record_definitions_keep_their_wire_values() { assert_eq!(OUTER_HEADER_LEN, 44); @@ -889,6 +1075,7 @@ fn record_definitions_keep_their_wire_values() { assert_eq!(APP_ERROR, 0xff); } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn feature_definitions_keep_their_wire_values() { assert_eq!(APP_FEATURES, 5); @@ -902,6 +1089,10 @@ fn feature_definitions_keep_their_wire_values() { assert_eq!(GuestFeatures::HOST_MAPPING_TABLE.bits(), 0x40); } +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn host_mapping_table_keeps_its_wire_values() { assert_eq!(MAX_HOST_MAPPINGS, 4096); @@ -914,6 +1105,10 @@ fn host_mapping_table_keeps_its_wire_values() { assert_eq!(MAPPING_FAILED, "mapping-failed"); } +#[cfg(any( + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn exec_extension_keeps_its_wire_values() { assert_eq!(EXEC_EXTENDED, 1); @@ -931,6 +1126,11 @@ fn exec_extension_keeps_its_wire_values() { assert_eq!(UNSUPPORTED_OPERATION, b"unsupported-operation"); } +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn exec_limits_support_linux_paths() { // A working directory may be as long as one argument, and Linux cannot @@ -1038,6 +1238,12 @@ fn host_mapping_count_splits_the_command_line_as_the_guest_init_does() { } } +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn both_agent_modes_announce_the_host_mapping_table() { assert_eq!(GuestFeatures::DIRECT_AGENT.bits(), 0x75); @@ -1055,6 +1261,7 @@ fn both_agent_modes_announce_the_host_mapping_table() { ); } +#[cfg(all(feature = "cli", feature = "testing"))] #[test] fn layered_sandboxes_need_per_execution_environments_and_mapping_tables() { // The CLI's exec sends working directories and environments, and its start sends the @@ -1074,6 +1281,12 @@ fn layered_sandboxes_need_per_execution_environments_and_mapping_tables() { ); } +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn host_exec_encoding_round_trips_through_the_guest_decoder() { let environment = strings(&["FOO=a b", "EMPTY="]); @@ -1098,6 +1311,12 @@ fn host_exec_encoding_round_trips_through_the_guest_decoder() { assert!(decoded.inherit_default_env); } +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn guest_exec_decoder_accepts_linux_bytes_and_the_c_cwd_boundary() { let mut payload = Vec::new(); @@ -1123,6 +1342,7 @@ fn guest_exec_decoder_accepts_linux_bytes_and_the_c_cwd_boundary() { /// Encodes an extended exec request field by field, so that its flags, environment count, and /// working-directory length may contradict what follows them. +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] fn raw_extended( flags: u16, environment_count: u16, @@ -1149,6 +1369,7 @@ fn raw_extended( payload } +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] #[test] fn guest_exec_decoder_accepts_what_the_c_agent_accepted() { fn with_environment(flags: u16, entries: &[&[u8]]) -> GuestExecPayload { @@ -1186,6 +1407,7 @@ fn guest_exec_decoder_accepts_what_the_c_agent_accepted() { } } +#[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] #[test] fn guest_exec_decoder_refuses_what_the_c_agent_refused() { // The agent decodes with its own byte-oriented decoder, so each refusal that the host @@ -1271,6 +1493,12 @@ fn guest_exec_decoder_refuses_what_the_c_agent_refused() { /// same message, except where they differ by design: the host's requires each string to be UTF-8 /// as soon as it reads it, and they word the refusals of an entry's name differently, the host's /// naming the variable that repeats. +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn exec_decoders_refuse_strings_with_the_same_messages() { const PROGRAM: &[u8] = b"/bin/true"; @@ -1426,6 +1654,11 @@ fn exec_decoders_refuse_strings_with_the_same_messages() { } } +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn host_maps_encoding_round_trips_through_the_guest_decoder() { let maps = [ @@ -1458,6 +1691,11 @@ fn host_maps_encoding_round_trips_through_the_guest_decoder() { ); } +#[cfg(any( + feature = "cli", + feature = "testing", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] #[test] fn guest_maps_decoder_accepts_non_utf8_linux_paths() { let mut payload = encode_maps_payloads( @@ -1527,6 +1765,12 @@ fn maps_decoders_refuse_entries_with_the_same_messages() { /// Returns bytes near the vectors of `kinds`: the bytes of one of them, with up to four bytes /// overwritten, perhaps truncated, and with up to eight bytes appended. +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] fn near_vectors(kinds: &'static [&'static str]) -> impl Strategy> { let encodings: Vec<_> = vectors(NVXC_V1) .into_iter() @@ -1554,6 +1798,12 @@ fn near_vectors(kinds: &'static [&'static str]) -> impl Strategy }) } +#[cfg(all( + feature = "testing", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] proptest! { /// The host's and the guest's `EXEC` and `MAPS` decoders return, never panic, on arbitrary /// bytes and on bytes near their vectors, which reach past the checks of the headers. diff --git a/nvx-rs/src/testing/fake_openvmm.rs b/nvx-rs/src/testing/fake_openvmm.rs index 8004c62e6..a6da98ec8 100644 --- a/nvx-rs/src/testing/fake_openvmm.rs +++ b/nvx-rs/src/testing/fake_openvmm.rs @@ -75,7 +75,7 @@ //! and holds them until it exits; it reads them whole, and records what each held, with its role //! and path, in `fake-openvmm--blocks.json` next to the kernel. The //! `fake_block_delay_ms=MS` token delays the opens. Its guest advertises the features of the -//! sandbox agent, [`GuestFeatures::SANDBOX_AGENT`]. With `--microvm-lifecycle managed`, it serves +//! sandbox agent, `GuestFeatures::SANDBOX_AGENT`. With `--microvm-lifecycle managed`, it serves //! the control console as above, and maps the sandbox's shares as above too: from the children of //! the aggregate export at `/run/nvx/hostfs/root`, as many as `nvx_maps=` announces, which `MAPS` //! requests deliver; it refuses a `--mount` share and `nvx_share=` tokens. With diff --git a/nvx-rs/src/testing/fake_openvmm/tests.rs b/nvx-rs/src/testing/fake_openvmm/tests.rs index 39d2ec29f..ea78459ad 100644 --- a/nvx-rs/src/testing/fake_openvmm/tests.rs +++ b/nvx-rs/src/testing/fake_openvmm/tests.rs @@ -20,6 +20,12 @@ fn shells_raise_shlvl_like_busybox() { assert_eq!(fresh[0], ("SHLVL".to_owned(), "1".to_owned())); } +#[cfg(all( + feature = "cli", + feature = "guest", + target_os = "linux", + target_arch = "x86_64" +))] #[test] fn the_current_guest_advertises_every_required_feature() { assert_eq!(GUEST_FEATURES, GuestFeatures::REQUIRED); diff --git a/scripts/test-layout-allowlist.json b/scripts/test-layout-allowlist.json index 31193c0d5..ad2e99f86 100644 --- a/scripts/test-layout-allowlist.json +++ b/scripts/test-layout-allowlist.json @@ -14,7 +14,6 @@ "nvx-rs/src/host/network/address.rs": 2, "nvx-rs/src/host/platform/windows/spawn.rs": 1, "nvx-rs/src/host/session.rs": 5, - "nvx-rs/src/host/vm.rs": 2, - "nvx-rs/src/protocol/app.rs": 6 + "nvx-rs/src/host/vm.rs": 2 } }