Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/run-nvx-microvm-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ jobs:

with open(sys.argv[1], encoding="utf-8") as stream:
config = json.load(stream)
if config["format"] != 8 or config["export"]["owner"] != "caller":
if config["format"] != 9 or config["export"]["owner"] != "caller":
sys.exit(f"managed sandbox did not persist caller ownership: {config}")
EOF
errors=$(mktemp)
Expand Down Expand Up @@ -686,7 +686,7 @@ jobs:
("/opt/hostedtoolcache", "", "ro", ["nvx-denied"], "directory"),
("/results/output.txt", "", "rw", [], "file"),
]
if config["format"] != 8 or "mounts" in config or shares != expected:
if config["format"] != 9 or "mounts" in config or shares != expected:
sys.exit(f"managed sandbox did not persist every share: {config}")
EOF
nvx-rs/target/release/nvx-rs sandbox start --state-dir "${state}"
Expand Down Expand Up @@ -815,7 +815,7 @@ jobs:
["nvx-writable"],
),
]
if config["format"] != 8 or "mounts" in config or shares != expected:
if config["format"] != 9 or "mounts" in config or shares != expected:
sys.exit(f"managed sandbox did not persist the access policy: {config}")
EOF
nvx-rs/target/release/nvx-rs sandbox start --state-dir "${state}"
Expand Down Expand Up @@ -1117,7 +1117,7 @@ jobs:
$denied = ($child.denied | ForEach-Object { $_.Substring($child.root.Length + 1).Replace("\", "/") }) -join ";"
"$($bind.target),$($bind.source),$mode,$denied,$kind"
}) -join " "
if ($config.format -ne 8 -or $shares -ne "/workspace,,rw,nvx-denied,directory /config/settings.json,,ro,,file /opt/hostedtoolcache,,ro,nvx-denied,directory /results/output.txt,,rw,,file") {
if ($config.format -ne 9 -or $shares -ne "/workspace,,rw,nvx-denied,directory /config/settings.json,,ro,,file /opt/hostedtoolcache,,ro,nvx-denied,directory /results/output.txt,,rw,,file") {
throw "managed sandbox did not persist every share: $($config | ConvertTo-Json -Compress -Depth 5)"
}
try {
Expand Down Expand Up @@ -1215,7 +1215,7 @@ jobs:
function Get-Relative($paths) {
($paths | ForEach-Object { $_.Substring($child.root.Length + 1).Replace("\", "/") }) -join ","
}
if ($config.format -ne 8 -or @($config.export.children).Count -ne 1 -or @($config.export.binds).Count -ne 1 -or $bind.target -ne "/workspace" -or $bind.readOnly -or (Get-Relative $child.denied) -ne "nvx-denied" -or (Get-Relative $child.allowed) -ne "nvx-denied/nvx-allowed" -or (Get-Relative $child.write) -ne "nvx-writable") {
if ($config.format -ne 9 -or @($config.export.children).Count -ne 1 -or @($config.export.binds).Count -ne 1 -or $bind.target -ne "/workspace" -or $bind.readOnly -or (Get-Relative $child.denied) -ne "nvx-denied" -or (Get-Relative $child.allowed) -ne "nvx-denied/nvx-allowed" -or (Get-Relative $child.write) -ne "nvx-writable") {
throw "managed sandbox did not persist the access policy: $($config | ConvertTo-Json -Compress -Depth 5)"
}
try {
Expand Down
8 changes: 4 additions & 4 deletions doc/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -1162,13 +1162,13 @@ inside a writable part of a share. On Windows, it refuses a path that only a
verbatim `\\?\` path names, such as that of a directory whose name ends in a
period or a space, which the path without the prefix would not name. The
shares' children follow their resolved host paths. A managed sandbox stores
the plan in its configuration, in format 8, and each `start` checks it again,
the plan in its typed configuration, in format 9. Each `start` checks it again,
as `run` does once it has verified the distro layer: it refuses a host path
that no longer holds a directory or regular file as recorded, a recorded path
whose file identity changed, and shares whose host paths came to overlap, such
as through a hard link or a bind mount. NVX no longer reads configurations of
formats 2 to 6, which recorded the shares as requested; deprovision such a
sandbox and provision it again.
as through a hard link or a bind mount. NVX no longer reads earlier
configuration formats, including those that recorded the shares as requested;
deprovision such a sandbox and provision it again.

#### Access policy

Expand Down
10 changes: 7 additions & 3 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,11 @@ never after a dry run, a restore, an option that `nvx-rs` refuses, or Ctrl-C.
```text
nvx-rs/target/release/nvx-rs sandbox
[{run,provision,start,exec,stop,deprovision}]
[--layer ROLE,PATH,EROFS_UUID]...
[--layer ROLE,PATH,IMAGE_UUID]
[--layer-format {erofs,gpt}]
[--scratch PATH]
[--upper {scratch,ram}]
[--ram-upper-percent PERCENT]
[--state-dir PATH]
[--entrypoint PATH]
[--arg VALUE]...
Expand Down Expand Up @@ -299,9 +301,11 @@ do, and their names cannot be abbreviated either.
| Option | Default | Description |
| --- | --- | --- |
| `{run,provision,start,exec,stop,deprovision}` | `run` | Select a one-shot run or a managed lifecycle operation. |
| `--layer ROLE,PATH,EROFS_UUID` | required for `run` and `provision` | Attach the `distro` EROFS layer. Only `distro` is accepted. The UUID must be lowercase hexadecimal in canonical hyphenated form; braces, prefixes, uppercase, and unhyphenated forms are refused. |
| `--layer ROLE,PATH,IMAGE_UUID` | required for `run` and `provision` | Attach exactly one `distro` image by path. `run` takes EROFS; `provision` takes the format of `--layer-format`. The filesystem UUID or GPT disk GUID must be lowercase hexadecimal in canonical hyphenated form; braces, prefixes, uppercase, and unhyphenated forms are refused. |
| `--layer-format {erofs,gpt}` | `erofs` | Select provision's image format explicitly, without probing the file or inferring its format from its name. Other sandbox commands reject this option. |
| `--scratch PATH` | required for `run` and `provision` with a scratch upper | Attach a preformatted ext4 scratch image as the overlay's writable upper. `--upper ram` refuses it. |
| `--upper {scratch,ram}` | `scratch` | Select the overlay's writable upper for `run` or `provision`: `scratch`, the image of `--scratch`, or `ram`, a tmpfs in guest memory with no scratch image, which loses the workload's writes when the VM stops. A RAM upper holds at most half of the guest's RAM; see [Overlay upper](run.md#overlay-upper). |
| `--upper {scratch,ram}` | `scratch` | Select the overlay's writable upper for `run` or `provision`: `scratch`, the image of `--scratch`, or `ram`, a tmpfs in guest memory with no scratch image, which loses the workload's writes when the VM stops. The default RAM cap is half of guest memory; provision can set it with `--ram-upper-percent`. See [Overlay upper](run.md#overlay-upper). |
| `--ram-upper-percent PERCENT` | `50` | Set provision's RAM upper cap to an ASCII decimal integer from 1 through 90 percent of VM memory. Requires `--upper ram`; other sandbox commands reject this option. The typed configuration records the cap, image format and path, and upper kind, which each start reads unchanged. Older configuration formats are refused rather than migrated. |
| `--state-dir PATH` | required for managed operations | Select persistent sandbox state. One-shot `run` rejects this option. |
| `--entrypoint PATH` | `/bin/sh` | Select an absolute workload entrypoint without whitespace, control characters, double quotes, or any UTF-8 byte that Linux treats as whitespace. |
| `--arg VALUE` | none | Append one nonempty entrypoint argument without whitespace, control characters, double quotes, or any UTF-8 byte that Linux treats as whitespace. Repeat to pass multiple arguments. |
Expand Down
10 changes: 6 additions & 4 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2859,13 +2859,15 @@ The state directory is the directory of the sandbox's
sandbox shares, and `config.json` is the CLI's own record beside them. It also
records the [distro binding](#distro-bindings) and the
[scratch binding](#checked-handoff) of the sandbox, and its network as the
typed `SandboxNetwork`, in the serde form of the plan's network, in place of
the network options. `nvx-rs` refuses to start a sandbox that `nvx.py` or an
earlier `nvx-rs` provisioned, which has no bindings or no typed network:
typed `SandboxNetwork`, in the serde form of the plan's network. The typed
configuration names one EROFS or GPT image by path and identity, and a scratch
upper by path or a RAM upper with its cap from 1 through 90 percent, 50 by
default. `nvx-rs` refuses earlier configuration formats rather than migrating
them:

| File | Contents |
| --- | --- |
| `config.json` | The provisioned sandbox, in format 8, with the export plan of its shares under `export`, `null` for none, its upper under `upper`, `scratch` or `ram`, the binding of its distro layer under `distro_binding`, its scratch image under `scratch` and that image's binding under `scratch_binding`, both `null` for an upper in RAM, and its network under `network`, `null` for none, written by `serde_json`'s pretty printer with the members of every object sorted by name, with mode 0600, through a temporary file. `start` reads it as strict JSON, as egress policy files are read, into the typed fields of format 8, and also reads the format 7 that earlier builds wrote, which records no `upper` and keeps the upper on scratch, and the format 1 that they wrote for a sandbox without shares, with `mount` `null` and no `export` |
| `config.json` | The provisioned sandbox, in format 9 only: `image` holds its format, path and identity; `upper` is tagged as scratch with its path or RAM with `max_memory_percent`. `distro_binding` binds the image, and `scratch_binding` binds scratch or is `null` for RAM. The export and network are typed, `null` when absent. An atomic pretty-JSON write uses mode 0600, and start reads the same strict typed fields without a legacy reader. |
| `sandbox.lock` | The store's lock, which serializes the lifecycle transitions |
| `launch.json` | The store's journal of a start in progress, or of one that was interrupted |
| `runtime.json` | The store's runtime record of the running VM, in its format 4: the OpenVMM process, by its ID, its start time, and the host's boot, its control endpoint, `STATE/control.sock` or `//./pipe/openvmm-microvm-HEX` on Windows, and the [runtime identity](#runtime-identity) of its start |
Expand Down
4 changes: 3 additions & 1 deletion nvx-rs/src/host/cli/sandbox/lifecycle/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Persistent sandboxes on the CLI's store and caller-owned [`crate::host::vm`] interface.
//!
//! Provision keeps today's configuration, image bindings, network and export. Start journals the
//! Provision records the typed image and upper, image bindings, network and export. Start journals the
//! prepared capability and outputs before spawn, records the verified process, and commits the
//! matching saved VM only after authenticated readiness and mappings. Every lifecycle command
//! takes the CLI store's recoverable lock; indeterminate launches retain their evidence.
Expand Down Expand Up @@ -58,6 +58,8 @@ pub(crate) struct ProvisionConfig<'a> {
pub(crate) memory_mib: u64,
/// `--cmdline`.
pub(crate) cmdline: &'a str,
/// The RAM upper's cap, or the default when omitted.
pub(crate) ram_upper_percent: Option<u8>,
}

/// The control features that the layered sandbox root must provide for the requests of
Expand Down
67 changes: 3 additions & 64 deletions nvx-rs/src/host/cli/sandbox/lifecycle/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,17 @@ use super::{
CONFIG_NAME, Found, REQUIRED_FEATURES, connect, find_vm, mismatched, require_config,
running_vm, runtime_for,
};
use crate::activation::{Journal, MachineIdMode, OverlayMode, View};
use crate::host::cli::deadline::Deadline;
use crate::host::cli::sandbox::lock::LifecycleLock;
use crate::host::cli::sandbox::state::{Config, read_config};
use crate::host::cli::sandbox::{Artifacts, error_with_cause};
use crate::host::cli::session::{ExecRequest, ExecResult, Session};
use crate::host::cli::{Exit, Result, output};
use crate::host::config::{
Boot, ConsoleAttachment, LayeredRoot, NetworkConfig, RootConfig, UpperConfig, VmConfig,
};
use crate::host::process::KILL_TIMEOUT;
use crate::host::runtime::Runtime;
use crate::host::sandbox::distro::DISTRO_POLICY_REVISION;
use crate::host::sandbox::scratch::CLAIM_RELEASE_WAIT;
use crate::host::sandbox::{SandboxLaunch, SandboxPlan};
use crate::host::vm::{self, SavedVm, VmDir};
use crate::net::Ipv4Cidr;
use crate::{ErrorCode, ErrorKind};

/// Starts a provisioned sandbox, as `start` does.
Expand Down Expand Up @@ -68,13 +62,11 @@ fn start_locked(
}
let config = Config::new(config)?;
let launch = config.launch()?;
let binding = config.distro_binding()?;
let binding = config.distro_binding();
let scratch_binding = config.scratch_binding()?;
let distro = binding
.check(launch.distro(), DISTRO_POLICY_REVISION)
.map_err(binding_error)?;
// Reading the network checks its policy again, as provision did, before anything launches.
let launch = launch.with_network(config.network()?);
let scratch = match (scratch_binding, launch.scratch()) {
(Some(scratch_binding), Some(scratch)) => Some(
scratch_binding
Expand All @@ -83,12 +75,12 @@ fn start_locked(
),
_ => None,
};
let plan = launch.bound_plan(&binding)?;
let plan = launch.bound_plan(binding)?;
// Keep provision's refusal of caller-owned nvx_* tokens, without launching its old plan.
plan.kernel_command_line(config.cmdline())?;
artifacts.require()?;
let runtime = runtime_for(artifacts, &config, artifacts.launch_identity()?)?;
let config = vm_config(&config, &launch, &plan)?;
let config = config.vm_config(&launch)?;
let dir = VmDir::open(state_dir).map_err(error_with_cause)?;
lock.clear_outputs()?;
let mut prepared = vm::create(&runtime, &config, None, &dir).map_err(error_with_cause)?;
Expand Down Expand Up @@ -177,59 +169,6 @@ fn start_locked(
Err(error)
}

fn vm_config(config: &Config, launch: &SandboxLaunch, plan: &SandboxPlan) -> Result<VmConfig> {
let (uid, gid) = launch.settings().workload_identity;
let mut command_line = format!("nvx_sandbox=1 nvx_mode=managed nvx_identity={uid}:{gid}");
if !config.cmdline().is_empty() {
command_line.push(' ');
command_line.push_str(config.cmdline());
}
let network = launch
.network()
.map(|network| -> Result<NetworkConfig> {
Ok(NetworkConfig {
guest_network: Some(
Ipv4Cidr::parse(network.guest_network())
.map_err(|error| Exit::error(ErrorCode::Unsupported, error.to_string()))?,
),
policy: network.policy().clone(),
})
})
.transpose()?;
Ok(VmConfig {
processors: 1,
memory_mib: config.memory_mib(),
root: RootConfig::Layered(LayeredRoot {
image: launch.distro().path().to_path_buf(),
format: plan.distro().image(),
gpt_first_partition_bound: None,
upper: match launch.scratch() {
Some(path) => UpperConfig::Scratch {
path: path.to_path_buf(),
journal: Journal::Allowed,
},
None => UpperConfig::Ram {
max_memory_percent: UpperConfig::DEFAULT_MAX_MEMORY_PERCENT,
},
},
overlay: OverlayMode::TrustedLowers,
}),
view: View {
hostname: launch.settings().hostname.as_bytes().to_vec(),
// As the sandbox agent did, nvx-init gives the workload a machine ID of the VM's own.
machine_id: MachineIdMode::Generated,
},
network,
filesystem: launch.export().cloned(),
console: ConsoleAttachment::Disconnected,
boot: Some(Boot {
command_line,
memory_capacity_mib: None,
capture: None,
}),
})
}

/// Returns the error of a start whose distro layer or scratch image fails the check of its
/// binding: a binding that no longer holds needs a new provision, which verifies the layer and
/// binds the image again, while a scratch image that another sandbox claims, and a failure of the
Expand Down
33 changes: 31 additions & 2 deletions nvx-rs/src/host/cli/sandbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use super::shell::format_command;
use super::{Exit, Result, output};
use crate::ErrorCode;
use crate::host::artifacts::{Guest, KERNEL_NAME, require_artifact};
use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding};
use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding, DistroFormat};
use crate::host::sandbox::mount::{plan_mounts, validate_mounts};
use crate::host::sandbox::options::{
SandboxOperation, SandboxOptions, sandbox_mounts, validate_sandbox_systemd_policy,
Expand Down Expand Up @@ -137,6 +137,20 @@ pub(crate) fn error_with_cause(error: crate::Error) -> Exit {
/// Runs the operation of `options`, as `command_sandbox` did.
pub(super) fn run(root: Option<&str>, options: Options) -> Result {
let operation = options.operation;
if !matches!(operation, SandboxOperation::Provision)
&& (options.layer_format.is_some() || options.ram_upper_percent.is_some())
{
return Err(Exit::error(
ErrorCode::Unsupported,
"--layer-format and --ram-upper-percent are only supported by provision",
));
}
if options.ram_upper_percent.is_some() && options.upper != Some(RootUpper::Ram) {
return Err(Exit::error(
ErrorCode::Unsupported,
"--ram-upper-percent requires --upper ram",
));
}
let checks = SandboxOptions {
operation,
dry_run: options.dry_run,
Expand Down Expand Up @@ -193,7 +207,21 @@ pub(super) fn run(root: Option<&str>, options: Options) -> Result {
}
RootUpper::Ram => SandboxUpper::RAM,
};
let launch = SandboxLaunch::new(options.layers.clone(), upper, settings)?;
let layers = options
.layers
.iter()
.map(|layer| {
Ok(match options.layer_format {
Some(DistroFormat::Gpt) => crate::host::sandbox::SandboxLayer::gpt(
layer.role(),
&path_string(layer.path())?,
layer.uuid(),
),
_ => layer.clone(),
})
})
.collect::<Result<Vec<_>>>()?;
let launch = SandboxLaunch::new(layers, upper, settings)?;
validate_mounts(&mounts)?;
launch.validated()?;
// The shares are planned once, from their resolved host paths, and `provision` records
Expand Down Expand Up @@ -230,6 +258,7 @@ pub(super) fn run(root: Option<&str>, options: Options) -> Result {
hypervisor: hypervisor(options.hypervisor),
memory_mib: options.memory_mib,
cmdline: &options.cmdline,
ram_upper_percent: options.ram_upper_percent,
},
options.timeout,
)
Expand Down
Loading
Loading