From b06d52fa085113e942d70a5424995e900dd08d18 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 22:35:14 -0800 Subject: [PATCH] cli: check the command line that a sandbox launches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sandbox start` and `sandbox run` refused a kernel command line through the old launch profile of `host::sandbox`: `check_command_line` built a `SandboxLaunch` of the configuration, with a `SandboxUpper` and `SandboxSettings`, bound its plan to the distro binding, and rendered the plan's kernel command line, which put the user's `--cmdline` first and left out the managed lifecycle's own tokens. `nvx-rs run` checked its shares against the kernel's budget with `host::sandbox`'s `SANDBOX_COMMAND_LINE_MAX_SIZE` and `export_command_line_fragment`. These were the CLI's last uses of the old launch. Both now check the command line that they launch, the one that the VM's `VmConfig` renders, with the CLI's own pieces: - `check_command_line` takes the sandbox's configuration and its `VmConfig`. It refuses the user's part as before: a NUL, and a token that the sandbox profile or `--mount` owns, `nvx_*` or `virtfs_*`, as Linux or the guest's init reads it, with the same messages. It then checks the launched line, with the bootstrap tokens of the shares and a NUL, against `host::config::COMMAND_LINE_BUDGET`, the same 1024 bytes as `SANDBOX_COMMAND_LINE_MAX_SIZE`, and reads the root's boot tokens from it with `RootLayout::parse`, with the old messages. - `export_tokens` renders the bootstrap tokens that OpenVMM appends for an export at `/run/nvx/hostfs/root`, as `export_command_line_fragment` rendered them, for both checks. The user's part now follows the root's tokens, which it can no longer hide, but it precedes the tokens that OpenVMM appends, the virtio-mmio devices of the root's disks among them. A `--` or an unclosed quote, as Linux reads them, would hide those, so the check refuses the line that Linux does not read to its end, where the old check refused one that hid the root's tokens, and its message names OpenVMM's tokens. That includes a `--` after a byte that only Linux takes for whitespace, such as the 0xa0 that ends `à`, which the control console's check of `VmConfig` does not see, since it splits at ASCII whitespace. The check appends a parameter that the user's part cannot name, since it holds no `nvx_*` token, and requires Linux to read it last. Closed double quotes stay refused as before: `sandbox run` refuses them with `VmConfig::check_command_line`, which it called already, and a start when it creates its VM. The user's part is checked as given, not trimmed, since the launched line holds it as given. `Config::hostname` loses its last caller and goes. Docs: `doc/usage.md` and `doc/run.md` say how `--cmdline` is checked. They still said that quoted values keep their whitespace and that a `--` or a quote hides the root's tokens. Tests: `starts_check_the_kernel_command_line_that_they_launch` fills the budget to the byte with and without shares, over scratch and in RAM, pins the tokens of a read-only export, and pins the refusals of a NUL, of a virtio-mmio device at the scratch disk's base beside a RAM upper, of a `--` after a space, after `à` and after a vertical tab, whatever parameter precedes it, and of an unclosed quote. `cold_share_command_lines_fit_the_kernel_budget_to_the_byte` takes the CLI's tokens and budget, and pins the tokens of a read-write export. `starts_refuse_the_kernel_tokens_of_the_sandbox_profile_and_of_its_mounts` adds a `--`, which a start refuses before it launches anything, as it refuses the reserved tokens. Part of #1120 (SL6.0f). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/run.md | 9 +- doc/usage.md | 2 +- nvx-rs/src/host/cli/run.rs | 16 +-- nvx-rs/src/host/cli/run/tests.rs | 13 ++- .../src/host/cli/sandbox/lifecycle/start.rs | 103 ++++++++++++----- .../host/cli/sandbox/lifecycle/start/tests.rs | 105 ++++++++++++++++++ nvx-rs/src/host/cli/sandbox/mod.rs | 18 ++- nvx-rs/src/host/cli/sandbox/state.rs | 5 - nvx-rs/tests/cli/lifecycle.rs | 11 +- 9 files changed, 226 insertions(+), 56 deletions(-) diff --git a/doc/run.md b/doc/run.md index 9edab4384..3c24659fb 100644 --- a/doc/run.md +++ b/doc/run.md @@ -1019,10 +1019,11 @@ treats as whitespace, including `0xa0`, while the workload's entrypoint and arguments, which nvx-init receives over the control session, may. Sandbox `--cmdline` uses Linux's quote and separator rules, and refuses reserved names even when quoted or written with hyphens instead of underscores. Validation -uses the same trimmed text that is rendered and also checks the guest bootstrap's -whitespace-only view, so reserved settings inside quoted values cannot override -the generated settings. It also -refuses a `--` or unclosed quote that hides the generated root tokens. +reads the text that the kernel command line holds, after the generated settings, +and also checks the guest bootstrap's whitespace-only view, so reserved settings +inside quoted values cannot override the generated settings. It also refuses a +`--` or unclosed quote that hides the tokens that OpenVMM appends, and double +quotes, which the VM control console cannot represent. nvx-init runs the workload as a plain process in the VM's root, with no capabilities and `no_new_privs`, as it runs every [managed](#managed-lifecycle) execution. The workload always runs as the fixed diff --git a/doc/usage.md b/doc/usage.md index 3aba1ee65..d371f42a7 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -376,7 +376,7 @@ do, and their names cannot be abbreviated either. | `--network-proxy IPV4:TCP-PORT` | none | Make the network proxy-only: the workload reaches the HTTP or HTTPS proxy that listens on host `127.0.0.1:TCP-PORT` at `IPV4:TCP-PORT`, where `IPV4` must be the guest gateway, and nothing else. `TCP-PORT` can't be 53, where OpenVMM answers DNS at the gateway. Requires `--network-egress deny` and `--host-loopback deny`, and refuses egress rules, `--network-ingress allow`, and forwards. NVX gives the workload no proxy settings; see [Proxy-only networking](run.md#proxy-only-networking). | | `--host-loopback-forward PROTOCOL:HOST_PORT:GUEST_PORT` | none | Publish one TCP or UDP localhost port to the guest; repeat to add forwards. Requires `--host-loopback allow`. A UDP forward also requires egress allowed by default and no deny rule that covers the guest network, which carries its replies; OpenVMM lets the replies on a forwarded TCP connection through whatever the egress policy. | | `--outcome-report PATH` | none | Write a bounded local JSON outcome report: for `run`, OpenVMM's report of its VM, which the guest stopped after the workload; for `exec`, the workload's outcome. | -| `--cmdline TEXT` | empty | Append non-sandbox kernel parameters, which `nvx-rs` reads as Linux does: split at whitespace, which includes the `0xa0` in `à`, outside double quotes, with names that match whether they spell a dash or an underscore. Quoted values, such as `a="b c"`, keep their whitespace. Parameters whose names start with `nvx_` or `virtfs_` are reserved and refused, even inside a quoted value, and so is a `--` or unclosed quote that hides the sandbox's own tokens; see [Experimental single-workload sandbox](run.md#experimental-single-workload-sandbox). The guest powers off if its kernel applies a clock parameter that the time ABI forbids, such as `tsc=unstable`. | +| `--cmdline TEXT` | empty | Append non-sandbox kernel parameters, which `nvx-rs` reads as Linux does: split at whitespace, which includes the `0xa0` in `à`, outside double quotes, with names that match whether they spell a dash or an underscore. Parameters whose names start with `nvx_` or `virtfs_` are reserved and refused, even inside a quoted value, and so are a `--` or unclosed quote that hides the tokens that OpenVMM appends, and double quotes, which the VM control console cannot represent; see [Experimental single-workload sandbox](run.md#experimental-single-workload-sandbox). The guest powers off if its kernel applies a clock parameter that the time ABI forbids, such as `tsc=unstable`. | | `--dry-run` | off | For `run` only, print the generated OpenVMM microVM command without launching it. Other operations reject this option. | `run` and `start` install the release of a Git checkout that holds no diff --git a/nvx-rs/src/host/cli/run.rs b/nvx-rs/src/host/cli/run.rs index ab9f18c93..7d9bad90e 100644 --- a/nvx-rs/src/host/cli/run.rs +++ b/nvx-rs/src/host/cli/run.rs @@ -54,7 +54,8 @@ use super::download::{self, Hold}; use super::network::{NativeRules, NetworkOptions}; use super::sandbox::{ self, HYPERVISORS, MAX_MEMORY_MIB, MOUNT_OWNERS, copy_openvmm_outcome, error_with_cause, - hypervisor, path_string, path_text, sandbox_network, validate_outcome_destination, + export_tokens, hypervisor, path_string, path_text, sandbox_network, + validate_outcome_destination, }; use super::session::{AttachmentCancellation, Session}; use super::shell::format_command; @@ -63,16 +64,17 @@ use super::{Exit, Result, check_interrupt, output}; use crate::ErrorCode; use crate::activation::{MachineIdMode, View}; use crate::host::artifacts::{ArtifactDir, Guest, require_artifact}; -use crate::host::config::{Boot, ConsoleAttachment, NicConfig, RootConfig, VmConfig}; +use crate::host::config::{ + Boot, COMMAND_LINE_BUDGET, ConsoleAttachment, NicConfig, RootConfig, VmConfig, +}; use crate::host::filesystem::{ self, ExportPlan, ExportRequest, Mapping, Owner, PathList, ReservedTargets, Target, }; use crate::host::platform::{self, Transport}; use crate::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; -use crate::host::sandbox::SANDBOX_COMMAND_LINE_MAX_SIZE; use crate::host::sandbox::mount::{ - MountAccess, MountKind, MountOwner, POLICY_KINDS, PolicyKind, export_command_line_fragment, - mount_kind, parse_mount_value, require_mount_owner_supported, + MountAccess, MountKind, MountOwner, POLICY_KINDS, PolicyKind, mount_kind, parse_mount_value, + require_mount_owner_supported, }; use crate::host::sandbox::options::{SandboxOperation, validate_run_share_targets}; use crate::host::vm::{self, VmDir}; @@ -665,9 +667,7 @@ fn cold_vm_config( }; if let Some(export) = &config.filesystem { let line = config.kernel_command_line().map_err(error_with_cause)?; - if line.len() + export_command_line_fragment(export).len() + 1 - > SANDBOX_COMMAND_LINE_MAX_SIZE - { + if line.len() + export_tokens(export).len() + 1 > COMMAND_LINE_BUDGET { return Err(Exit::error( ErrorCode::Unsupported, "the --mount shares and --cmdline exceed the 1024-byte budget of the kernel command \ diff --git a/nvx-rs/src/host/cli/run/tests.rs b/nvx-rs/src/host/cli/run/tests.rs index 26fb615a5..176dfa747 100644 --- a/nvx-rs/src/host/cli/run/tests.rs +++ b/nvx-rs/src/host/cli/run/tests.rs @@ -66,11 +66,12 @@ fn cold_share_command_lines_fit_the_kernel_budget_to_the_byte() { }; let rules = options.network.egress_rules().unwrap(); let base = cold_config(&options, &rules).unwrap(); - let fragment = export_command_line_fragment(base.filesystem.as_ref().unwrap()); - let room = SANDBOX_COMMAND_LINE_MAX_SIZE - - base.kernel_command_line().unwrap().len() - - fragment.len() - - 2; + let fragment = export_tokens(base.filesystem.as_ref().unwrap()); + assert_eq!( + fragment, + " virtfs_dir=/run/nvx/hostfs/root virtfs_tag=microvm virtfs_mode=rw virtfs_aggregate=1" + ); + let room = COMMAND_LINE_BUDGET - base.kernel_command_line().unwrap().len() - fragment.len() - 2; for unicode in [false, true] { options.cmdline = if unicode { "\u{e9}".repeat(room / 2) + &"x".repeat(room % 2) @@ -80,7 +81,7 @@ fn cold_share_command_lines_fit_the_kernel_budget_to_the_byte() { let fitting = cold_config(&options, &rules).unwrap(); assert_eq!( fitting.kernel_command_line().unwrap().len() + fragment.len() + 1, - SANDBOX_COMMAND_LINE_MAX_SIZE + COMMAND_LINE_BUDGET ); options.cmdline.push('x'); assert!(cold_config(&options, &rules).is_err()); diff --git a/nvx-rs/src/host/cli/sandbox/lifecycle/start.rs b/nvx-rs/src/host/cli/sandbox/lifecycle/start.rs index be0b5d1ec..6125bc8bc 100644 --- a/nvx-rs/src/host/cli/sandbox/lifecycle/start.rs +++ b/nvx-rs/src/host/cli/sandbox/lifecycle/start.rs @@ -27,14 +27,14 @@ use crate::host::cli::sandbox::state::{ Config, RecordedDistro, RecordedScratch, Source, read_config, serialize_launch, }; use crate::host::cli::sandbox::{ - Artifacts, Options, error_with_cause, path_string, refuse_own_files, + Artifacts, Options, error_with_cause, export_tokens, path_string, refuse_own_files, }; use crate::host::cli::session::{ExecRequest, ExecResult, Session}; use crate::host::cli::shell::format_command; use crate::host::cli::snapshots::Store; use crate::host::cli::store::StateFile; use crate::host::cli::{Exit, Result, hex, io_error, output, random_bytes}; -use crate::host::config::{RootConfig, UpperConfig, VmConfig}; +use crate::host::config::{COMMAND_LINE_BUDGET, RootConfig, UpperConfig, VmConfig}; use crate::host::platform; use crate::host::process::KILL_TIMEOUT; use crate::host::report::Report; @@ -43,10 +43,11 @@ use crate::host::restore_driver::{self, Cancel, ControlRestoreError, Workload}; use crate::host::runtime::{ArtifactIdentity, Runtime, RuntimeIdentity}; use crate::host::sandbox::distro::DISTRO_POLICY_REVISION; use crate::host::sandbox::scratch::{CLAIM_RELEASE_WAIT, CheckedScratch}; -use crate::host::sandbox::{SandboxLaunch, SandboxSettings, SandboxUpper}; use crate::host::session::{ExecEvent, SessionError}; use crate::host::snapshot::OPENVMM_DIR; use crate::host::vm::{self, Launched, Prepared, SavedVm, VmDir}; +use crate::protocol::cmdline::{self, Parameter}; +use crate::protocol::root::RootLayout; use crate::protocol::workload::WorkloadIdentity; use crate::protocol::{self, ExitCategory, HostMap, WorkloadEnvironment}; use crate::{ErrorCode, ErrorKind}; @@ -133,8 +134,8 @@ fn start_locked( ), _ => None, }; - // Keep provision's refusal of caller-owned nvx_* tokens, without launching its old plan. - check_command_line(&config)?; + let vm_config = config.vm_config()?; + check_command_line(&config, &vm_config)?; artifacts.require()?; let runtime = runtime_for(artifacts, &config, artifacts.launch_identity()?)?; let restore = source.as_ref().map(|source| Restore { @@ -142,8 +143,15 @@ fn start_locked( identity: config.workload_identity(), forward_status, }); - let config = config.vm_config()?; - let started = launch_locked(lock, &runtime, config, restore, scratch, timeout, vm::spawn); + let started = launch_locked( + lock, + &runtime, + vm_config, + restore, + scratch, + timeout, + vm::spawn, + ); drop(distro); started } @@ -574,8 +582,8 @@ pub(crate) fn run( let recorded = record(distro, scratch, provision).and_then(|config| { // As `start` does, refuse the user's own nvx_* tokens, and the command line that the // launch would refuse, such as one that holds double quotes. - check_command_line(&config)?; let vm_config = config.vm_config()?; + check_command_line(&config, &vm_config)?; vm_config.check_command_line().map_err(error_with_cause)?; Session::exec_payload(request)?; Ok((config, vm_config)) @@ -768,26 +776,65 @@ fn record( Ok(config) } -/// Refuses the user's kernel command line of `config` as the plan of the sandbox profile refuses -/// it: a NUL; a token that the profile or the `--mount` option owns, an `nvx_*` or `virtfs_*` one, -/// as Linux or the guest's init reads it; and a line that, with the boot tokens of the root and -/// the bootstrap tokens of the shares, exceeds its 1024-byte x86 budget, or conflicts with or hides -/// those of the root. It builds that plan from the configuration: the one part of a start and a -/// run that still goes through the old `host::sandbox` launch. -fn check_command_line(config: &Config) -> Result { - let upper = match config.scratch() { - Some(scratch) => SandboxUpper::scratch_at(scratch), - None => SandboxUpper::RAM, - }; - let settings = SandboxSettings { - hostname: config.hostname().to_owned(), - workload_identity: config.workload_identity(), - ..SandboxSettings::default() - }; - SandboxLaunch::new(vec![config.distro()], upper, settings)? - .with_export(config.export().cloned())? - .bound_plan(config.distro_binding())? - .kernel_command_line(config.cmdline())?; +/// Refuses the kernel command line that a start or a run of the sandbox of `config` launches, as +/// `vm`, its VM's configuration, renders it: one whose user's part holds a NUL, or a token that +/// the sandbox profile or the `--mount` option owns, an `nvx_*` or `virtfs_*` one, as Linux or +/// the guest's init reads it; one that, with the bootstrap tokens that OpenVMM appends for the +/// shares and a NUL, exceeds [`COMMAND_LINE_BUDGET`]; one from which the guest cannot read the +/// boot tokens of its root, as when the user's part adds a virtio-mmio device at the scratch +/// disk's base beside a RAM upper; and one that hides from Linux the tokens that OpenVMM appends +/// to it, with a `--` or an unclosed quote, as Linux reads them. +fn check_command_line(config: &Config, vm: &VmConfig) -> Result { + let refused = |message: &str| Exit::error(ErrorCode::Unsupported, message); + let user = config.cmdline().as_bytes(); + if user.contains(&0) { + return Err(refused("kernel command line contains an embedded NUL")); + } + let guest = cmdline::init_tokens(user).map(|token| Parameter { + name: token.name, + value: token.value, + }); + for Parameter { name, .. } in cmdline::parameters(user).chain(guest) { + let owner = if name.starts_with(b"nvx_") || name.starts_with(b"nvx-") { + "the sandbox profile" + } else if name.starts_with(b"virtfs_") || name.starts_with(b"virtfs-") { + "the sandbox --mount option" + } else { + continue; + }; + let name = String::from_utf8_lossy(name); + return Err(refused(&format!("{name} is owned by {owner}"))); + } + let line = vm.kernel_command_line().map_err(error_with_cause)?; + let shares = vm + .filesystem + .as_ref() + .map_or(0, |plan| export_tokens(plan).len()); + if line.len() + shares + 1 > COMMAND_LINE_BUDGET { + return Err(refused( + "sandbox kernel command line exceeds its 1024-byte x86 budget; use shorter arguments \ + and kernel parameters", + )); + } + RootLayout::parse(line.as_bytes()).map_err(|error| { + refused(&format!( + "sandbox kernel command line conflicts with the boot tokens of its root: {error}" + )) + })?; + // Linux must read the tokens that OpenVMM appends, as it reads one after the line whose name + // the user's part, which holds no `nvx_*` token, cannot spell. + let appended = format!("{line} nvx_appended"); + if !matches!( + cmdline::parameters(appended.as_bytes()).last(), + Some(Parameter { + name: b"nvx_appended", + value: None + }) + ) { + return Err(refused( + "sandbox kernel command line hides the tokens that OpenVMM appends to it", + )); + } Ok(()) } diff --git a/nvx-rs/src/host/cli/sandbox/lifecycle/start/tests.rs b/nvx-rs/src/host/cli/sandbox/lifecycle/start/tests.rs index 9f4825883..6a7ca9d85 100644 --- a/nvx-rs/src/host/cli/sandbox/lifecycle/start/tests.rs +++ b/nvx-rs/src/host/cli/sandbox/lifecycle/start/tests.rs @@ -4,6 +4,7 @@ use std::fs; use std::path::PathBuf; use super::*; +use crate::host::filesystem::ExportPlan; use crate::host::paths::resolve; use crate::host::sandbox::distro::DistroBinding; use crate::host::sandbox::layer::{LayerRole, SandboxLayer}; @@ -81,6 +82,110 @@ fn run_checks_again_that_its_shares_do_not_overlap() { ); } +/// A start and a run check the kernel command line that they launch: the user's part, after the +/// boot tokens of the root and of the workload, and the bootstrap tokens that OpenVMM appends for +/// the shares fill the 1024-byte budget to the byte, whatever the upper and the shares, and the +/// guest must read the root's boot tokens from it. +#[test] +fn starts_check_the_kernel_command_line_that_they_launch() { + let directory = tempfile::tempdir().unwrap(); + let root = resolve(directory.path()).unwrap(); + let path = |name: &str| root.join(name).to_str().unwrap().to_owned(); + for (name, content) in [ + ("distro.erofs", "distro"), + ("scratch.ext4", "scratch"), + ("settings.json", "{}"), + ("output.log", ""), + ] { + fs::write(path(name), content).unwrap(); + } + let layer = SandboxLayer::new( + LayerRole::Distro, + &path("distro.erofs"), + "11111111-1111-1111-1111-111111111111", + ); + let distro = RecordedDistro::new(&layer, DistroBinding::verify(&layer).unwrap()).unwrap(); + let scratch = PathBuf::from(path("scratch.ext4")); + let settings = format!("/etc/settings.json,{}", path("settings.json")); + let output = format!("/var/log/output.log,{},rw", path("output.log")); + let export = |shares: &[String]| plan_mounts(&sandbox_mounts(shares, &[], None).unwrap()); + let read_only = export(std::slice::from_ref(&settings)).unwrap(); + let writable = export(&[settings, output]).unwrap(); + assert_eq!( + export_tokens(read_only.as_ref().unwrap()), + " virtfs_dir=/run/nvx/hostfs/root virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" + ); + let configure = |cmdline: &str, scratch: Option<&Path>, export: Option<&ExportPlan>| { + let config = ProvisionConfig { + hypervisor: "kvm", + memory_mib: 256, + cmdline, + ram_upper_percent: None, + hostname: DEFAULT_HOSTNAME, + workload_identity: DEFAULT_WORKLOAD_IDENTITY, + export, + network: None, + }; + let config = record(&distro, scratch, &config).unwrap(); + let vm = config.vm_config().unwrap(); + (config, vm) + }; + let refusal = |cmdline: &str, scratch: Option<&Path>, export: Option<&ExportPlan>| { + let (config, vm) = configure(cmdline, scratch, export); + let error = check_command_line(&config, &vm).unwrap_err(); + assert_eq!(error.code(), Some(ErrorCode::Unsupported), "{error:?}"); + error.message().unwrap_or_default().to_owned() + }; + for upper in [None, Some(scratch.as_path())] { + for export in [None, read_only.as_ref(), writable.as_ref()] { + let (_, vm) = configure("", upper, export); + let fragment = export.map(export_tokens).unwrap_or_default(); + let room = + COMMAND_LINE_BUDGET - vm.kernel_command_line().unwrap().len() - fragment.len() - 2; + let (config, vm) = configure(&"x".repeat(room), upper, export); + check_command_line(&config, &vm).unwrap(); + assert_eq!( + vm.kernel_command_line().unwrap().len() + fragment.len() + 1, + COMMAND_LINE_BUDGET + ); + assert_eq!( + refusal(&"x".repeat(room + 1), upper, export), + "sandbox kernel command line exceeds its 1024-byte x86 budget; use shorter \ + arguments and kernel parameters" + ); + } + } + assert_eq!( + refusal("quiet\0", None, None), + "kernel command line contains an embedded NUL" + ); + // A `--` as Linux reads it, whose whitespace includes the 0xa0 that ends `à` and a vertical + // tab, and an unclosed quote hide the tokens that OpenVMM appends after the user's part, also + // after a parameter of any name. + for user in [ + "quiet -- debug", + "quiet \u{e0}--", + "quiet\u{b}--", + "x --", + "x\u{b}--", + "a=\"b c", + ] { + assert_eq!( + refusal(user, None, None), + "sandbox kernel command line hides the tokens that OpenVMM appends to it", + "{user:?}" + ); + } + // The user's part may add a virtio-mmio device, which the guest reads as the kernel does: one + // at the scratch disk's base conflicts with a RAM upper. + assert_eq!( + refusal("virtio_mmio.device=0x1000@0xd0006000:22", None, None), + "sandbox kernel command line conflicts with the boot tokens of its root: a RAM upper \ + takes no scratch block device, but the command line adds a virtio-mmio device at \ + 0xd0006000" + ); +} + /// A run removes its VM directory with the staging file of an outcome report that OpenVMM did /// not finish writing, as one that the run forced down can leave, and keeps a directory that /// holds a file of another name. diff --git a/nvx-rs/src/host/cli/sandbox/mod.rs b/nvx-rs/src/host/cli/sandbox/mod.rs index 8055b6250..c67b49bc5 100644 --- a/nvx-rs/src/host/cli/sandbox/mod.rs +++ b/nvx-rs/src/host/cli/sandbox/mod.rs @@ -34,8 +34,9 @@ use crate::host::runtime::RuntimeIdentity; use crate::host::sandbox::DEFAULT_ENTRYPOINT; use crate::host::sandbox::distro::{DistroBinding, DistroFormat}; use crate::host::sandbox::layer::SandboxLayer; -use crate::host::sandbox::mount::{plan_mounts, validate_mounts}; +use crate::host::sandbox::mount::{MOUNT_TAG, plan_mounts, validate_mounts}; use crate::host::sandbox::options::{SandboxOperation, SandboxOptions, sandbox_mounts}; +use crate::protocol::export::{EXPORT_DIRECTORY, ExportMode}; use crate::protocol::root::RootUpper; pub(crate) use self::lifecycle::ProvisionConfig; @@ -130,6 +131,21 @@ pub(crate) fn error_with_cause(error: crate::Error) -> Exit { } } +/// Returns the bootstrap tokens that OpenVMM appends to a VM's kernel command line for `plan`, +/// its export at [`EXPORT_DIRECTORY`], as [`HostExport`](crate::protocol::export::HostExport) +/// reads them: they take part of the line's budget. +pub(crate) fn export_tokens(plan: &ExportPlan) -> String { + let mode = if plan.children.iter().any(|child| child.writable) { + ExportMode::ReadWrite + } else { + ExportMode::ReadOnly + }; + format!( + " virtfs_dir={EXPORT_DIRECTORY} virtfs_tag={MOUNT_TAG} virtfs_mode={mode} \ + virtfs_aggregate=1" + ) +} + /// Runs the operation of `options`, as `command_sandbox` did. pub(super) fn run(root: Option<&str>, options: Options) -> Result { run_from(root, options, None) diff --git a/nvx-rs/src/host/cli/sandbox/state.rs b/nvx-rs/src/host/cli/sandbox/state.rs index 4a68ba0c6..dfe30b68e 100644 --- a/nvx-rs/src/host/cli/sandbox/state.rs +++ b/nvx-rs/src/host/cli/sandbox/state.rs @@ -610,11 +610,6 @@ impl Config { } } - /// Returns the workload's hostname. - pub(crate) fn hostname(&self) -> &str { - &self.hostname - } - /// Returns the workload's UID and GID. pub(crate) fn workload_identity(&self) -> (u32, u32) { (self.workload_uid, self.workload_gid) diff --git a/nvx-rs/tests/cli/lifecycle.rs b/nvx-rs/tests/cli/lifecycle.rs index fe2c7db8b..6ffecd2fb 100644 --- a/nvx-rs/tests/cli/lifecycle.rs +++ b/nvx-rs/tests/cli/lifecycle.rs @@ -1384,9 +1384,10 @@ fn unusable_guest_ram(root: &Root) { } /// A start refuses a configuration whose kernel command line holds a token of the sandbox profile, -/// `nvx_*`, or of `--mount`, `virtfs_*`, as Linux or the guest's init reads the line, before it -/// launches OpenVMM. `provision` records the profile's tokens that the configuration of the VM -/// leaves to the caller, such as `nvx_entrypoint`, and the start refuses those too. +/// `nvx_*`, or of `--mount`, `virtfs_*`, as Linux or the guest's init reads the line, or a `--`, +/// which would hide from Linux the tokens that OpenVMM appends, before it launches OpenVMM. +/// `provision` records the profile's tokens that the configuration of the VM leaves to the caller, +/// such as `nvx_entrypoint`, and the start refuses those too. #[test] fn starts_refuse_the_kernel_tokens_of_the_sandbox_profile_and_of_its_mounts() { let root = Root::new(); @@ -1412,6 +1413,10 @@ fn starts_refuse_the_kernel_tokens_of_the_sandbox_profile_and_of_its_mounts() { Some("virtfs_tag=microvm"), "virtfs_tag is owned by the sandbox --mount option", ), + ( + Some("quiet -- debug"), + "sandbox kernel command line hides the tokens that OpenVMM appends to it", + ), ] { if let Some(cmdline) = cmdline { let mut config = config.clone();