diff --git a/.github/workflows/run-nvx-microvm-tests.yml b/.github/workflows/run-nvx-microvm-tests.yml index 131bbc7fb..330c69c3c 100644 --- a/.github/workflows/run-nvx-microvm-tests.yml +++ b/.github/workflows/run-nvx-microvm-tests.yml @@ -516,7 +516,7 @@ jobs: with open(sys.argv[1], encoding="utf-8") as stream: config = json.load(stream) - if config["format"] != 3 or config["mount"]["owner"] != "caller": + if config["format"] != 7 or config["export"]["owner"] != "caller": sys.exit(f"managed sandbox did not persist caller ownership: {config}") EOF python3 scripts/nvx.py sandbox start --state-dir "${state}" @@ -627,26 +627,33 @@ jobs: --workload-user 65534:65534 python3 - "${state}/config.json" <<'EOF' import json + import os import sys with open(sys.argv[1], encoding="utf-8") as stream: config = json.load(stream) - shares = [ - ( - mount["guest_target"], - mount["access"], - mount["denied_paths"], - mount["kind"], + # The export plan binds each share's root, in the order of the + # targets' depth, then the targets. + export = config["export"] + shares = [] + for bind in export["binds"]: + child = export["children"][bind["child"]] + shares.append( + ( + bind["target"], + bind["source"], + "ro" if bind["readOnly"] else "rw", + [os.path.relpath(path, child["root"]) for path in child["denied"]], + "file" if child.get("file") else "directory", + ) ) - for mount in config["mounts"] - ] expected = [ - ("/workspace", "rw", ["nvx-denied"], "directory"), - ("/opt/hostedtoolcache", "ro", ["nvx-denied"], "directory"), - ("/config/settings.json", "ro", [], "file"), - ("/results/output.txt", "rw", [], "file"), + ("/workspace", "", "rw", ["nvx-denied"], "directory"), + ("/config/settings.json", "", "ro", [], "file"), + ("/opt/hostedtoolcache", "", "ro", ["nvx-denied"], "directory"), + ("/results/output.txt", "", "rw", [], "file"), ] - if config["format"] != 6 or "mount" in config or shares != expected: + if config["format"] != 7 or "mounts" in config or shares != expected: sys.exit(f"managed sandbox did not persist every share: {config}") EOF python3 scripts/nvx.py sandbox start --state-dir "${state}" @@ -753,20 +760,26 @@ jobs: --workload-user 65534:65534 python3 - "${state}/config.json" <<'EOF' import json + import os import sys with open(sys.argv[1], encoding="utf-8") as stream: config = json.load(stream) - shares = [ - ( - mount["guest_target"], - mount["access"], - mount["denied_paths"], - mount["allowed_paths"], - mount["writable_paths"], + # The export plan holds the policy paths resolved inside the share. + export = config["export"] + shares = [] + for bind in export["binds"]: + child = export["children"][bind["child"]] + shares.append( + ( + bind["target"], + "ro" if bind["readOnly"] else "rw", + *( + [os.path.relpath(path, child["root"]) for path in child[name]] + for name in ("denied", "allowed", "write") + ), + ) ) - for mount in config["mounts"] - ] expected = [ ( "/workspace", @@ -776,7 +789,7 @@ jobs: ["nvx-writable"], ), ] - if config["format"] != 5 or "mount" in config or shares != expected: + if config["format"] != 7 or "mounts" in config or shares != expected: sys.exit(f"managed sandbox did not persist the access policy: {config}") EOF python3 scripts/nvx.py sandbox start --state-dir "${state}" @@ -1091,8 +1104,17 @@ jobs: Initialize-Shares Invoke-Nvx sandbox provision --state-dir $state --hypervisor "${{ inputs.backend }}" --layer $layer --scratch $scratch @shareOptions --workload-user 65534:65534 $config = Get-Content -Raw (Join-Path $state "config.json") | ConvertFrom-Json - $shares = ($config.mounts | ForEach-Object { "$($_.guest_target),$($_.access),$($_.kind)" }) -join " " - if ($config.format -ne 6 -or $shares -ne "/workspace,rw,directory /opt/hostedtoolcache,ro,directory /config/settings.json,ro,file /results/output.txt,rw,file") { + # The export plan binds each share's root, in the order of the + # targets' depth, then the targets. + $shares = ($config.export.binds | ForEach-Object { + $bind = $_ + $child = $config.export.children[$bind.child] + $mode = if ($bind.readOnly) { "ro" } else { "rw" } + $kind = if ($child.file) { "file" } else { "directory" } + $denied = ($child.denied | ForEach-Object { $_.Substring($child.root.Length + 1).Replace("\", "/") }) -join ";" + "$($bind.target),$($bind.source),$mode,$denied,$kind" + }) -join " " + if ($config.format -ne 7 -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 { @@ -1197,8 +1219,13 @@ jobs: Initialize-Share Invoke-Nvx sandbox provision --state-dir $state --hypervisor "${{ inputs.backend }}" --layer $layer --scratch $scratch @shareOptions --workload-user 65534:65534 $config = Get-Content -Raw (Join-Path $state "config.json") | ConvertFrom-Json - $mount = $config.mounts[0] - if ($config.format -ne 5 -or $config.mounts.Count -ne 1 -or $mount.access -ne "rw" -or ($mount.denied_paths -join ",") -ne "nvx-denied" -or ($mount.allowed_paths -join ",") -ne "nvx-denied/nvx-allowed" -or ($mount.writable_paths -join ",") -ne "nvx-writable") { + # The export plan holds the policy paths resolved inside the share. + $child = @($config.export.children)[0] + $bind = @($config.export.binds)[0] + function Get-Relative($paths) { + ($paths | ForEach-Object { $_.Substring($child.root.Length + 1).Replace("\", "/") }) -join "," + } + if ($config.format -ne 7 -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 { diff --git a/aci_edge_sandboxes/README.md b/aci_edge_sandboxes/README.md index 839a14201..cc9b15595 100644 --- a/aci_edge_sandboxes/README.md +++ b/aci_edge_sandboxes/README.md @@ -853,7 +853,10 @@ translation, for example to turn a host working directory into a or singles out, such as a read-write path inside a read-only one, must not contain colons, backslashes, links, or whitespace other than spaces, and no name in it may begin or end with a space. -- Mapped paths must exist and be directories or regular files. A path listed +- Mapped paths must exist and be directories or regular files. On Windows, a + mapped or denied path that only a verbatim `\\?\` path names, such as that + of a directory whose name ends in a period or a space, is refused, since the + path without the prefix would name another object, or none. A path listed both read-only and read-write is mapped read-only. A read-only path, directory or file, must not resolve to a path inside a read-write directory, but a read-write path may lie inside a read-only directory. diff --git a/aci_edge_sandboxes/src/openvmm/mod.rs b/aci_edge_sandboxes/src/openvmm/mod.rs index 41e0d56f3..026dbca81 100644 --- a/aci_edge_sandboxes/src/openvmm/mod.rs +++ b/aci_edge_sandboxes/src/openvmm/mod.rs @@ -146,7 +146,7 @@ use std::thread; use std::time::Duration; use nvx::Artifacts; -use nvx::host::filesystem::{self, ExportPlan, ExportRequest, PathList}; +use nvx::host::filesystem::{self, ExportPlan, ExportRequest, Mapping, PathList, ReservedTargets}; use nvx::host::instance::{self, Execution, Found, LockedInstance, WorkloadOutcome}; use nvx::host::launch::{LaunchSpec, WorkloadIdentity}; use nvx::host::report::Report; @@ -325,21 +325,22 @@ fn egress_rule(rule: &NetworkRule) -> network::EgressRule { } } -/// Plans the export of the contract's filesystem section, under the contract's field names. +/// Plans the export of the contract's filesystem section, under the contract's field names, at +/// the guest paths of the host-path convention in the blockless root. fn export_plan(policy: &FilesystemPolicy) -> Result> { + let list = |name, paths| PathList { name, paths }; + let mappings: Vec> = Mapping::convention( + list("filesystem.readonlyPaths", &policy.readonly_paths), + true, + ) + .chain(Mapping::convention( + list("filesystem.readwritePaths", &policy.readwrite_paths), + false, + )) + .collect(); Ok(filesystem::plan(&ExportRequest { - read_only: PathList { - name: "filesystem.readonlyPaths", - paths: &policy.readonly_paths, - }, - read_write: PathList { - name: "filesystem.readwritePaths", - paths: &policy.readwrite_paths, - }, - denied: PathList { - name: "filesystem.deniedPaths", - paths: &policy.denied_paths, - }, + denied: list("filesystem.deniedPaths", &policy.denied_paths), + ..ExportRequest::new(&mappings, ReservedTargets::BLOCKLESS_ROOT) })?) } diff --git a/aci_edge_sandboxes/src/openvmm/state.rs b/aci_edge_sandboxes/src/openvmm/state.rs index 9bd7d1a9c..25e0dbd82 100644 --- a/aci_edge_sandboxes/src/openvmm/state.rs +++ b/aci_edge_sandboxes/src/openvmm/state.rs @@ -35,10 +35,11 @@ use crate::model::{HostLoopbackForward, NetworkPolicy}; /// Version of `sandbox.json`, the configuration that this crate records, which holds the /// sandbox's [`ExportPlan`] in the form of `nvx`. /// -/// Version 1 described sandboxes assembled from image layers and a scratch disk, and version 2 +/// Version 1 described sandboxes assembled from image layers and a scratch disk, version 2 /// exported the mapped host paths as one directory whose bind mounts the kernel command line -/// listed. -pub(crate) const STATE_FORMAT: u32 = 3; +/// listed, and version 3 recorded plans whose exported directories could be hidden but for their +/// allowed paths. +pub(crate) const STATE_FORMAT: u32 = 4; /// Backend key recorded in every sandbox. pub(crate) const BACKEND_KEY: &str = "openvmm"; @@ -340,18 +341,26 @@ mod tests { "binds": [{ "source": "", "target": "/host/work", "readOnly": true }], }); unsupported(&legacy); + // Version 3 recorded whether OpenVMM hid each exported directory. + legacy["format"] = 3.into(); + legacy["filesystem"] = serde_json::json!({ + "children": [{ "root": "/host/work", "writable": false, "hidden": false, + "denied": [], "deniedIdentities": [], "allowed": [], "write": [] }], + "binds": [{ "child": 0, "source": "", "target": "/host/work", "readOnly": true }], + }); + unsupported(&legacy); } #[test] fn records_of_the_current_format_keep_loading() { - // A record that the crate wrote before NVX moved to `nvx-rs`, with a file and a - // directory child, pinned identities, and every optional field. + // A record with a file and a directory child, pinned identities, and every optional + // field. let root = tempfile::tempdir().unwrap(); let store = StateStore::open(root.path()).unwrap(); let id = SandboxId::generate().unwrap(); store.create(&id, &record()).unwrap(); let written = serde_json::json!({ - "format": 3, + "format": 4, "backend": "openvmm", "network": { "egress": { @@ -364,9 +373,8 @@ mod tests { "filesystem": { "children": [ { "root": "/host/work/config.json", "file": true, "writable": false, - "hidden": false, "denied": [], "deniedIdentities": [], "allowed": [], - "write": [] }, - { "root": "/host/work/out", "writable": true, "hidden": false, + "denied": [], "deniedIdentities": [], "allowed": [], "write": [] }, + { "root": "/host/work/out", "writable": true, "denied": ["/host/work/out/secret"], "deniedIdentities": [{ "device": 1, "index": 2 }], "allowed": [], "write": [] } diff --git a/aci_edge_sandboxes/tests/openvmm_fake.rs b/aci_edge_sandboxes/tests/openvmm_fake.rs index 68971cd73..49cf80474 100644 --- a/aci_edge_sandboxes/tests/openvmm_fake.rs +++ b/aci_edge_sandboxes/tests/openvmm_fake.rs @@ -1995,8 +1995,8 @@ fn recorded_read_only_paths_inside_read_write_directories_are_refused_at_start() ..FilesystemPolicy::default() }); let sandbox_id = nvx.provision(&request).unwrap().sandbox_id; - // An earlier crate recorded a read-only src inside the read-write work like this nesting of - // read-write paths, but with a read-only bind, which only the guest kept read-only. + // A record with a read-only bind of src inside the read-write work, which only the guest + // would keep read-only, as nothing that the planner makes holds. let mut record = state_json(&fixture, &sandbox_id, "sandbox.json"); assert_eq!( record["filesystem"]["binds"][1]["source"], "src", diff --git a/doc/run.md b/doc/run.md index 03bc4c3bd..542a73ed2 100644 --- a/doc/run.md +++ b/doc/run.md @@ -807,8 +807,8 @@ mode applies to the whole file. A relative `--mount-deny`, `--mount-allow`, or `--mount-write` path is resolved inside its share's host directory. With one share, every such path -applies to it. With several, each applies to the `--mount` before it and must -name a path inside that share's directory. The guest targets and the host +applies to it. With several, each applies to the `--mount` before it. Either +way, it must name a path inside its share's directory. The guest targets and the host paths of the shares must not equal or contain one another, because one share could otherwise hide another or reach its files under a different access mode, so a shared file may lie beside a shared directory, but not inside it. @@ -853,18 +853,22 @@ without its shares. An `rw` share supports the symbolic links that package managers and language toolchains create; see [virtio-fs host mapping](#virtio-fs-host-mapping) for their semantics. The -existing OpenVMM file-identity policy applies to each share. A managed -sandbox stores each share's absolute host path, kind, mode, denied, allowed, -and writable paths and the ownership mode in its configuration, and reattaches -every share on each `start`, which refuses a host path that no longer holds a -directory or regular file as recorded. A configuration with several shares -uses format 4, which earlier NVX releases reject rather than start without the -other shares. -A configuration with allowed or writable paths uses format 5, which earlier -NVX releases reject rather than start a share without its access policy. -A configuration with a shared file uses format 6, which earlier NVX releases -reject rather than start without the file. A host path with commas keeps the -configuration's format, because earlier NVX releases already refuse it. +existing OpenVMM file-identity policy applies to each share. +`run` and `provision` plan the shares once, with the planner that every NVX +caller uses: it resolves each host path and access-policy path, which must +exist, refuses what OpenVMM or the guest would refuse, and records the file +identity of each shared or denied path that the workload could move, those +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 7, and 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. #### Access policy @@ -889,8 +893,10 @@ or link reaches the share: writable path to a read-only file fails with `EXDEV` (`Invalid cross-device link`), so the link cannot make the file writable. -Each path must exist inside the share's host directory, must not cross a -symbolic link or junction, and is attributed to a share like `--mount-deny`. +Each path must exist inside the share's host directory, and is attributed to +a share like `--mount-deny`. `sandbox` resolves it once, when it plans the +share, so it may cross a symbolic link or junction to a path inside the same +directory; `run` passes it to OpenVMM, which refuses a path that crosses one. Its names may contain spaces, but not begin or end with one, nor contain other whitespace, colons, or backslashes. Allowed paths follow the share's write policy: without `--mount-write`, an diff --git a/nvx-rs/README.md b/nvx-rs/README.md index fac89abde..798678e21 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -77,7 +77,7 @@ nvx-rs/ | `nvx::host::launch` | `LaunchSpec` and OpenVMM's command line: the managed lifecycle, the `WorkloadIdentity`, the 1024-byte kernel command line, its reserved parameters, the 32,767-character limit of a Windows command line, and `LaunchSpec::check`, which every command line applies to the memory size, the non-root identity, the hostname, the guest network, and the kernel parameters | | `nvx::host::network` | `NetworkPolicy` and `openvmm_arguments`, the one lowering of a network posture into OpenVMM options, for the CLI and external orchestrators: the egress default and rules, ingress, `HostLoopback` access, denied, `Outbound` from the guest alone, or both ways, and its forwards. A proxy-only policy, `NetworkPolicy::proxy_only`, has one way out, a `NetworkProxy` that listens on a port of host loopback, which the guest reaches at the same port of its gateway, and whose URL as the guest sees it `NetworkProxy::url` gives; the lowering refuses a proxy beside any other way out. Egress rules are lowered exactly into OpenVMM's IPv4 and IPv6 rules by the lowering of egress policy files, with at most 256 rules for each action | | `nvx::host::network::policy` | Egress policy files: `compile_policy_file`, `compile_policy_bytes`, and `compile_policy` compile one to a `CompiledEgressPolicy` of native rules; see [Egress policy files](#egress-policy-files) | -| `nvx::host::filesystem` | The `ExportPlan` of host paths through OpenVMM's aggregate virtio-fs device, the host-mapping table, the reserved guest targets, and `guest_path` and `resolve_guest_path` (also at `nvx::`) | +| `nvx::host::filesystem` | The planner of every NVX caller: `plan` turns an `ExportRequest`, the mapped host paths at explicit guest targets or at those of the host-path convention, their denied, allowed, and write paths, and the owner, into the `ExportPlan` of host paths through OpenVMM's aggregate virtio-fs device, for a root's `ReservedTargets`, the blockless root's or the container root's: its children, its ordered binds, which give the host-mapping table, and the identities that each start checks again; and `guest_path` and `resolve_guest_path` (also at `nvx::`) | | `nvx::host::store` | `SandboxStore`: the runtime records of one sandbox in a private directory with one lock, which every host that supervises the sandbox shares. The `RuntimeRecord` names the running VM: the `ProcessRef` of its VMM, its control endpoint, and the `RuntimeIdentity` that it was launched with, the identities of its OpenVMM, kernel, and initramfs from install or staging metadata and its boot and control contract versions, which decide whether another build of the host may reattach to it. The `LaunchJournal` of a launch holds a marker written before the spawn, the VMM's `ProcessRef` right after it, and an inherited claim on the VMM's log, which proves that a launch that never published its endpoint has exited, so that recovery ends an interrupted launch and signals only a VMM that it holds through a verified `Process`. Records are JSON in one versioned format, and records in any other format fail rather than being migrated. The caller declares the files that it keeps in the directory, its configuration record, whose presence means that the sandbox is provisioned, and any others, as `CallerFiles`, and takes the lifecycle locks with them: `SandboxStore::lock_for_provision` creates the directory, again if a removal deletes it meanwhile, `SandboxStore::lock_existing` fails with `ErrorKind::NotFound` once it is gone, and a holder that leaves the directory without the configuration record removes its lock file as it releases the lock. `LockedStore::remove` removes a sandbox that no VMM can run in, for a caller that declares its configuration record: the store's files, the caller's, and the directory, unless it holds entries that are neither, which it refuses and keeps, and `Lingering` waits for open files, such as the lock files of other transitions, that keep the emptied directory; both remove the directory only while the path still names it. The store also owns the file in which the VMM stages its outcome report, which a VMM that is killed while it writes the report leaves, and removes it once no process of its launch runs | | `nvx::host::instance` | `Instance`: a VM in a sandbox store of its own, which `nvx::host::store`, `nvx::host::supervisor`, and `nvx::host::session` drive, and in which the caller keeps the files that it declares as `CallerFiles`. Under the store's lifecycle lock of an existing sandbox, `LockedInstance::find` reattaches to the VM, ends a start that was interrupted, and reports a VM of another runtime identity as `Mismatched` rather than driving it; `LockedInstance::start` launches OpenVMM through the supervisor with a fresh capability, under the caller's `RuntimeIdentity`, waits until the guest is ready and has mounted the mapped paths, and commits the launch; `LockedInstance::stop` stops the VM with a forced fallback; `LockedInstance::terminate` stops a `Mismatched` VM by identity-verified forced termination, without a control session or its outcome report; and `LockedInstance::remove` removes a stopped instance, with the caller's files and its directory, through the store. `Instance::exec` runs workloads without the lock and pumps their output. `write_json` and `read_json` keep the configuration records that callers store beside an instance | | `nvx::host::nvxs` | `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. 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 partial record | @@ -85,7 +85,7 @@ nvx-rs/ | `nvx::host::supervisor` | The supervisor of a sandbox's VMM, beneath the CLI, `Instance`, and external orchestrators, which keep the configuration, the authorization, and the application protocol. `launch` runs the store's launch steps under the sandbox's lock: it writes the journal's marker, spawns OpenVMM with the claimed log, records its `ProcessRef` right after, and returns a `Starting` launch that the caller commits once the VMM serves its control endpoint, or abandons. A launch that fails never leaves an unrecorded VMM running. `VmmCommand::inherit` lists the descriptors (Linux) or handles (Windows) that the VMM inherits under their numbers, besides its standard streams, and it inherits nothing else of the caller's: on Linux, they must be close-on-exec, a flag that only the VMM's own process clears; on Windows, they join the VMM's explicit handle list. `Vmm::stop` runs one graceful step of the caller's and waits for the VMM's exit event until a deadline, and takes a bounded forced step through the VMM's verified identity only on request. `Vmm::terminate` takes that forced step alone, within the caller's bound, for a VM whose guest the caller must not ask anything, such as one of another runtime identity. `reattach` recovers an interrupted launch, and holds the recorded VMM again after the orchestrator restarts within the same host boot: `Live`, `Mismatched` with another runtime identity, `Exited`, or `Indeterminate` | | `nvx::host::seal` | `FileSeal`: what the host reports about an open file from its metadata alone, without reading it: the platform scheme, the device or volume serial number, the 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` | 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 | -| `nvx::host::sandbox` | The launch profile of `nvx.py sandbox`, ported from the one that `scripts/nvx_tools/sandbox.py` held: `SandboxLaunch`, with the EROFS distro `SandboxLayer`, the one layer that a sandbox takes, the ext4 scratch disk, the entrypoint, arguments, hostname, workload identity, and resource limits, and the `SandboxPlan` that it makes of them. `plan`: `SandboxPlan`, the typed plan of a cold layered launch, which is persistable in a versioned serde form: one read-only `distro` block device, an EROFS filesystem or a GPT disk with the identity that the guest expects of it, if any, an upper, the ext4 scratch disk or RAM, the launch settings, and optionally a `SandboxNetwork`, the guest network and its typed `NetworkPolicy`, whose proxy's URL as the guest sees it the plan reports. A plan knows only the `distro` and `scratch` `BlockRole`s, and gives, for every combination, its `--microvm-sandbox-block` and `--microvm-workload-identity` arguments, with no scratch block device for a RAM upper, and those of its network, which `nvx::host::network` lowers, and its kernel command line, with the boot tokens of `nvx::protocol::root`, within the 1024-byte budget that OpenVMM leaves it, refusing one whose root tokens the guest's parser refuses. `mount`: `SandboxMount` live shares of host directories and files, their guest targets and the reserved ones, their access policies and owner, the aggregate of several shares, and the checks of their host paths. `options`: the checks that `nvx.py sandbox` and `nvx.py run` apply to their options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` to shares, the systemd entrypoint check and the explicit verification of the distro layer, the targets of `nvx.py run` shares, and the managed exec environment; see [Python parity](#python-parity). `distro`: the `DistroBinding` of a distro layer, which explicit verification makes or a staging caller imports, and its check from metadata alone, which returns the layer open; see [Distro bindings](#distro-bindings). `scratch`: the `ScratchBinding` of a scratch image, its identity, kind, and length, and its check from metadata alone, which claims the image for one sandbox at a time and returns it open. `prepared`: `PreparedLaunch`, the launch of a plan with the checked files of its block devices, which renders the plan's OpenVMM arguments and kernel command line, naming the checked files as `/proc/self/fd/N` on Linux and by their final paths on Windows, and lists the descriptors that the VMM inherits through `VmmCommand::inherit`; see [Checked handoff](#checked-handoff) | +| `nvx::host::sandbox` | The launch profile of `nvx.py sandbox`, ported from the one that `scripts/nvx_tools/sandbox.py` held: `SandboxLaunch`, with the EROFS distro `SandboxLayer`, the one layer that a sandbox takes, the ext4 scratch disk, the entrypoint, arguments, hostname, workload identity, and resource limits, and the `SandboxPlan` that it makes of them. `plan`: `SandboxPlan`, the typed plan of a cold layered launch, which is persistable in a versioned serde form: one read-only `distro` block device, an EROFS filesystem or a GPT disk with the identity that the guest expects of it, if any, an upper, the ext4 scratch disk or RAM, the launch settings, the `ExportPlan` of its live shares, and optionally a `SandboxNetwork`, the guest network and its typed `NetworkPolicy`, whose proxy's URL as the guest sees it the plan reports. A plan knows only the `distro` and `scratch` `BlockRole`s, and gives, for every combination, its `--microvm-sandbox-block` and `--microvm-workload-identity` arguments, with no scratch block device for a RAM upper, and those of its network, which `nvx::host::network` lowers, and its kernel command line, with the boot tokens of `nvx::protocol::root`, within the 1024-byte budget that OpenVMM leaves it, refusing one whose root tokens the guest's parser refuses. `mount`: `SandboxMount` requests of live shares of host directories and files, their guest targets and the reserved ones, their access policies and owner, and the checks of their host paths; `plan_mounts`, which plans them through `nvx::host::filesystem` for the container root, and `boot_shares`, which renders a plan as the OpenVMM arguments and boot tokens of one share or of the aggregate of several. `options`: the checks that `nvx.py sandbox` and `nvx.py run` apply to their options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` to shares, the systemd entrypoint check and the explicit verification of the distro layer, the targets of `nvx.py run` shares, and the managed exec environment; see [Python parity](#python-parity). `distro`: the `DistroBinding` of a distro layer, which explicit verification makes or a staging caller imports, and its check from metadata alone, which returns the layer open; see [Distro bindings](#distro-bindings). `scratch`: the `ScratchBinding` of a scratch image, its identity, kind, and length, and its check from metadata alone, which claims the image for one sandbox at a time and returns it open. `prepared`: `PreparedLaunch`, the launch of a plan with the checked files of its block devices, which renders the plan's OpenVMM arguments and kernel command line, naming the checked files as `/proc/self/fd/N` on Linux and by their final paths on Windows, and lists the descriptors that the VMM inherits through `VmmCommand::inherit`; see [Checked handoff](#checked-handoff) | | `nvx::testing::fake_openvmm` | A fake OpenVMM that serves the control console and runs scripted workloads without a VM, for direct launches and for the managed and one-shot sandboxes of `nvx-rs sandbox` and `nvx.py sandbox` | | `nvx::host::cli` | Feature `cli`: the `nvx-rs` executable; see [The nvx-rs CLI](#the-nvx-rs-cli) | | `nvx::guest` | The programs that run in Linux guests; see [Guest binaries](#guest-binaries) | @@ -549,7 +549,7 @@ earlier `nvx-rs` provisioned, which has no bindings or no typed network: | File | Contents | | --- | --- | -| `config.json` | The provisioned sandbox, in the lowest of formats 1 to 6 that holds it, with the binding of its distro layer under `distro_binding`, that of its scratch image under `scratch_binding`, and its network under `network`, `null` for none, written as `json.dumps(value, indent=2, sort_keys=True)` writes it, with mode 0600, through a temporary file | +| `config.json` | The provisioned sandbox, in format 1 without shares and in format 7 with the export plan of its shares under `export`, with the binding of its distro layer under `distro_binding`, that of its scratch image under `scratch_binding`, and its network under `network`, `null` for none, written as `json.dumps(value, indent=2, sort_keys=True)` writes it, with mode 0600, through a temporary file | | `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 | @@ -741,6 +741,9 @@ the launch profile has rendered since the recording too, and their network options render through the typed policy, which `config.json` records under `network`: the scenarios whose options OpenVMM refused, or that combined a proxy with other access, now show the planner's refusals or valid policies. +The configurations of sandboxes with shares have since held the shares' export +plan, in format 7, and a policy path outside the host directory of a sandbox's +only share now fails before OpenVMM starts. Its `posix` and `windows` sections were recorded on each kind of host, where the script also played every scenario with `nvx-rs`, and each lifecycle with the Python implementation and `nvx-rs` taking diff --git a/nvx-rs/src/host/cli/sandbox/lifecycle.rs b/nvx-rs/src/host/cli/sandbox/lifecycle.rs index f210192f7..96b195d30 100644 --- a/nvx-rs/src/host/cli/sandbox/lifecycle.rs +++ b/nvx-rs/src/host/cli/sandbox/lifecycle.rs @@ -7,8 +7,9 @@ //! //! ```text //! / -//! config.json the launch that provision records, in formats 1 to 6, with the bindings -//! of its distro layer and of its scratch image, and its network +//! config.json the launch that provision records, in format 1 without shares and in +//! format 7 with the export plan of its shares, with the bindings of its +//! distro layer and of its scratch image, and its network //! sandbox.lock the store's lock, which serializes the lifecycle transitions //! launch.json the store's journal of a start in progress, or of an interrupted one //! runtime.json the store's runtime record of the running VM, in its format 4 @@ -28,7 +29,9 @@ //! `provision` verified, and that of the scratch image, which [`start`] checks from metadata //! alone, and the network as its typed policy, which [`start`] checks again, in place of the //! network options that `sandbox_lifecycle.py` recorded: it refuses to start a sandbox that -//! `nvx.py` or an earlier `nvx-rs` provisioned, which has no bindings or no typed network. +//! `nvx.py` or an earlier `nvx-rs` provisioned, which has no bindings or no typed network. It +//! records shares as the export plan that `provision` made, whose host objects [`start`] checks +//! again. //! //! Every transition but provision finds the sandbox's VM with [`supervisor::reattach`], as an //! orchestrator does after it restarts: it ends a start that was interrupted, holds a recorded diff --git a/nvx-rs/src/host/cli/sandbox/mod.rs b/nvx-rs/src/host/cli/sandbox/mod.rs index 9867ae2f7..11cc4b6c3 100644 --- a/nvx-rs/src/host/cli/sandbox/mod.rs +++ b/nvx-rs/src/host/cli/sandbox/mod.rs @@ -37,7 +37,7 @@ use crate::host::python::fs::os_error_message; use crate::host::python::path::PurePath; use crate::host::python::shell::format_command; use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding}; -use crate::host::sandbox::mount::{MountOwner, PolicyKind}; +use crate::host::sandbox::mount::{MountOwner, PolicyKind, plan_mounts, validate_mounts}; use crate::host::sandbox::options::{ PolicyPathOption, SandboxOperation, SandboxOptions, sandbox_mounts, validate_sandbox_systemd_policy, @@ -591,14 +591,17 @@ pub(super) fn run(root: Option<&str>, options: Options) -> Result { workload_identity: options.workload_user, memory_max: options.memory_max.clone(), pids_max: options.pids_max.clone(), - mounts, }; let launch = SandboxLaunch::new( options.layers.clone(), options.scratch.as_deref().unwrap_or_default(), settings, )?; + validate_mounts(&mounts)?; launch.validated()?; + // The shares are planned once, from their resolved host paths, and `provision` records + // the plan, whose identities each `start` checks again. + let launch = launch.with_export(plan_mounts(&mounts)?)?; // `run` and `provision` verify the distro layer explicitly, at the path given; // `provision` records its binding, which `start` checks without reading the layer again. let binding = validate_sandbox_systemd_policy(&launch)?; @@ -675,9 +678,8 @@ pub(super) fn run(root: Option<&str>, options: Options) -> Result { "one-shot sandbox execution rejects persistent --state-dir settings", )); } + let plan = run_plan(&launch, &binding)?; let [openvmm, kernel, initrd] = artifacts.require()?; - // The plan takes the format and identity of the distro layer from its binding. - let plan = launch.bound_plan(&binding)?; // A dry run launches nothing, so it shows the plan's own paths; a run launches through // the checked files, which it holds until OpenVMM exits. let (arguments, command_line, prepared) = if options.dry_run { @@ -732,6 +734,20 @@ pub(super) fn run(root: Option<&str>, options: Options) -> Result { } } +/// Checks the launch of a one-shot run again, and returns its plan with the format and identity of +/// the distro layer that `binding`, which the run's verification of the layer made, records. +/// +/// The verification read all of the layer after the shares were planned, so the launch must still +/// pass [`SandboxLaunch::validated`], as `provision` checks it before recording it and `start` +/// checks a recorded one: the directories and files of its export plan must still not overlap, by +/// path or through a hard link or bind mount, and must pass [`ExportPlan::verify`]. +/// +/// [`ExportPlan::verify`]: crate::host::filesystem::ExportPlan::verify +fn run_plan(launch: &SandboxLaunch, binding: &DistroBinding) -> Result { + launch.validated()?; + launch.bound_plan(binding).map_err(Exit::from) +} + /// Checks the distro layer of a one-shot run against `binding`, which the run's verification of /// the layer made, binds and claims its scratch image, and prepares the launch of `plan` with the /// checked files and the user's kernel command line `cmdline`. @@ -867,3 +883,6 @@ pub(crate) fn spawn_error(error: &std::io::Error, program: &str) -> String { let program = if cfg!(windows) { None } else { Some(program) }; crate::host::python::fs::os_error_message(error, program) } + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/cli/sandbox/state.rs b/nvx-rs/src/host/cli/sandbox/state.rs index 2fdc88c0e..8cad790f2 100644 --- a/nvx-rs/src/host/cli/sandbox/state.rs +++ b/nvx-rs/src/host/cli/sandbox/state.rs @@ -1,4 +1,4 @@ -//! The CLI's own record in a state directory, `config.json` in formats 1 to 6, and the JSON +//! The CLI's own record in a state directory, `config.json` in formats 1 and 7, and the JSON //! reading and writing of the former `sandbox_lifecycle.py`. //! //! - [`write_json`] writes `json.dumps(value, indent=2, sort_keys=True) + "\n"` as @@ -7,9 +7,11 @@ //! - [`read_json`] reads a file as `json.loads(path.read_text(encoding="utf-8"))` does, and //! accepts only an object whose version field equals, as `==` compares, one of the given //! versions. -//! - [`serialize_launch`] writes a provisioned launch in the lowest format that holds it, and -//! [`deserialize_launch`] reads it back with the conversions, defaults, and checks of -//! `_deserialize_launch`. +//! - [`serialize_launch`] writes a provisioned launch, in format 1 without shares and in format 7 +//! with the export plan of its shares, and [`deserialize_launch`] reads it back with the +//! conversions, defaults, and checks of `_deserialize_launch`. Formats 2 to 6 recorded the shares +//! as they were requested; `start` no longer reads them, and such a sandbox is provisioned +//! again. //! - Every configuration also records the [`DistroBinding`] of the sandbox's distro layer, under //! `distro_binding` in its serde form, which [`deserialize_binding`] reads back for `start` to //! check. `nvx.py` never wrote one, and `start` refuses a configuration without one. The @@ -30,6 +32,7 @@ use std::io::Write; use super::lifecycle::ProvisionConfig; use crate::host::cli::output::LINE_SEPARATOR; use crate::host::cli::{Exit, Result}; +use crate::host::filesystem::ExportPlan; use crate::host::python::builtins::{Int, repr}; use crate::host::python::dumps::{self, Value, dict}; use crate::host::python::fs::{self, os_error_message}; @@ -39,40 +42,26 @@ use crate::host::python::text::decode_utf8; use crate::host::python::value::{self, IntError, equals_int, py_int, py_str}; use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding}; use crate::host::sandbox::layer::{LayerRole, SandboxLayer}; -use crate::host::sandbox::mount::{ - MountAccess, MountKind, MountOwner, MountPolicy, SandboxMount, validate_mount_target, -}; use crate::host::sandbox::scratch::ScratchBinding; use crate::host::sandbox::{SandboxLaunch, SandboxNetwork, SandboxSettings}; -/// The configuration of a sandbox without shares. +/// The configuration of a sandbox without shares, whose `mount` is `null`. pub(crate) const CONFIG_FORMAT: u64 = 1; -/// The configuration of a sandbox with one share, which format-1 readers would start without it. -pub(crate) const MOUNT_CONFIG_FORMAT: u64 = 2; -/// The configuration of a sandbox with one share that the caller owns, which format-2 readers -/// would start as the VMM. -pub(crate) const OWNER_CONFIG_FORMAT: u64 = 3; -/// The configuration of a sandbox with several shares, listed under `mounts`. -pub(crate) const MULTI_MOUNT_CONFIG_FORMAT: u64 = 4; -/// The configuration of a sandbox whose shares have allowed or writable paths. -pub(crate) const POLICY_CONFIG_FORMAT: u64 = 5; -/// The configuration of a sandbox that shares a file, with the kind of each share. -pub(crate) const FILE_MOUNT_CONFIG_FORMAT: u64 = 6; +/// The configuration of a sandbox with shares, which records their export plan under `export`, +/// in its serde form. +pub(crate) const EXPORT_CONFIG_FORMAT: u64 = 7; /// Every configuration format that `start` reads. -pub(crate) const CONFIG_FORMATS: [u64; 6] = [ - CONFIG_FORMAT, - MOUNT_CONFIG_FORMAT, - OWNER_CONFIG_FORMAT, - MULTI_MOUNT_CONFIG_FORMAT, - POLICY_CONFIG_FORMAT, - FILE_MOUNT_CONFIG_FORMAT, -]; +pub(crate) const CONFIG_FORMATS: [u64; 2] = [CONFIG_FORMAT, EXPORT_CONFIG_FORMAT]; /// The member of a configuration that records the binding of the sandbox's distro layer. pub(crate) const DISTRO_BINDING: &str = "distro_binding"; /// The member of a configuration that records the binding of the sandbox's scratch image. pub(crate) const SCRATCH_BINDING: &str = "scratch_binding"; /// How deeply a recorded binding may nest its values, which its serde form nests three deep. const MAX_BINDING_DEPTH: usize = 8; +/// The member of a configuration that records the export plan of its shares. +pub(crate) const EXPORT: &str = "export"; +/// How deeply a recorded export plan may nest its values, which its serde form nests five deep. +const MAX_EXPORT_DEPTH: usize = 8; /// The member of a configuration that records the sandbox's network, or `null` for none. pub(crate) const NETWORK: &str = "network"; /// How deeply a recorded network may nest its values, which its serde form nests eight deep, to @@ -225,35 +214,15 @@ pub(crate) fn read_json( Ok(document) } -/// Returns the format of the configuration of a sandbox with `mounts`: the lowest that holds -/// them, as `_config_format` chooses it. -pub(crate) fn config_format(mounts: &[SandboxMount]) -> u64 { - if mounts.iter().any(|mount| mount.kind() == MountKind::File) { - FILE_MOUNT_CONFIG_FORMAT - } else if mounts - .iter() - .any(|mount| !mount.policy().allowed.is_empty() || !mount.policy().writable.is_empty()) - { - POLICY_CONFIG_FORMAT - } else if mounts.len() > 1 { - MULTI_MOUNT_CONFIG_FORMAT - } else if mounts.len() == 1 && mounts[0].owner() == MountOwner::Caller { - OWNER_CONFIG_FORMAT - } else if mounts.len() == 1 { - MOUNT_CONFIG_FORMAT - } else { - CONFIG_FORMAT +/// Returns the format of the configuration of a sandbox with `export`, the export plan of its +/// shares, if any. +pub(crate) fn config_format(export: Option<&ExportPlan>) -> u64 { + match export { + Some(_) => EXPORT_CONFIG_FORMAT, + None => CONFIG_FORMAT, } } -/// Returns whether a format lists every share under `mounts`. -fn lists_mounts(format: u64) -> bool { - matches!( - format, - MULTI_MOUNT_CONFIG_FORMAT | POLICY_CONFIG_FORMAT | FILE_MOUNT_CONFIG_FORMAT - ) -} - /// Returns `path.resolve()` as text. fn resolved(path: &std::path::Path) -> Result { let text = crate::host::cli::sandbox::path_string(path)?; @@ -324,16 +293,6 @@ impl RecordedScratch { } } -/// Returns a list of strings. -fn strings(values: &[String]) -> Value { - Value::List( - values - .iter() - .map(|value| Value::from(value.as_str())) - .collect(), - ) -} - /// Returns the configuration that `provision` records for `launch` and the other options, as /// `_serialize_launch` builds it, with `distro`, the launch's distro layer at the path that /// `provision` verified and records, and its binding, and `scratch`, the launch's scratch image at @@ -344,7 +303,7 @@ pub(crate) fn serialize_launch( scratch: &RecordedScratch, config: &ProvisionConfig<'_>, ) -> Result { - let format = config_format(launch.mounts()); + let format = config_format(launch.export()); // The distro layer is the one layer that a launch holds. let layer = &distro.layer; let layers = vec![dict([ @@ -391,22 +350,18 @@ pub(crate) fn serialize_launch( .and_then(python_value) .ok_or_else(|| Exit::error("cannot record the scratch binding"))?; members.insert(SCRATCH_BINDING.to_owned(), binding); - if lists_mounts(format) { - let mut mounts = Vec::new(); - for mount in launch.mounts() { - mounts.push(serialize_mount( - mount, - format != MULTI_MOUNT_CONFIG_FORMAT, - format == FILE_MOUNT_CONFIG_FORMAT, - )?); + match launch.export() { + Some(export) => { + let export = serde_json::to_value(export) + .ok() + .as_ref() + .and_then(python_value) + .ok_or_else(|| Exit::error("cannot record the export plan of the shares"))?; + members.insert(EXPORT.to_owned(), export); + } + None => { + members.insert("mount".to_owned(), Value::Null); } - members.insert("mounts".to_owned(), Value::List(mounts)); - } else { - let mount = match launch.mounts().first() { - Some(mount) => serialize_mount(mount, false, false)?, - None => Value::Null, - }; - members.insert("mount".to_owned(), mount); } Ok(value) } @@ -523,41 +478,6 @@ pub(crate) fn deserialize_network(document: &Document) -> Result Result { - let absolute = mount.absolute()?; - let mut value = dict([ - ("guest_target", Value::from(absolute.guest_target())), - ( - "host_path", - Value::from(crate::host::cli::sandbox::path_string( - absolute.host_path(), - )?), - ), - ("access", Value::from(absolute.access().as_str())), - ("denied_paths", strings(&absolute.policy().denied)), - ("owner", Value::from(absolute.owner().as_str())), - ]); - let Value::Dict(members) = &mut value else { - unreachable!("dict builds a dict") - }; - if policy { - members.insert( - "allowed_paths".to_owned(), - strings(&absolute.policy().allowed), - ); - members.insert( - "writable_paths".to_owned(), - strings(&absolute.policy().writable), - ); - } - if kind { - members.insert("kind".to_owned(), Value::from(absolute.kind().as_str())); - } - Ok(value) -} - /// Why reading a recorded launch failed. enum Malformed { /// The `KeyError`, `TypeError`, or `ValueError` that `_deserialize_launch` reports as a @@ -611,124 +531,38 @@ fn iterate(document: &Document, id: NodeId) -> std::result::Result std::result::Result, Malformed> { - let value = member(document, mount, key).ok_or(Malformed::Malformed)?; - let Node::Array(items) = document.node(value) else { - return Err(Malformed::Malformed); - }; - items - .iter() - .map(|&item| py_str(document, item).ok_or(Malformed::Malformed)) - .collect() -} - -/// Reads one recorded share, as `_deserialize_mount` does, and checks it as -/// `SandboxMount.__post_init__` does. -fn deserialize_mount( - document: &Document, - id: NodeId, - policy: bool, - kind: bool, -) -> std::result::Result { - if !matches!(document.node(id), Node::Object(_)) { - return Err(Malformed::Malformed); - } - let guest_target = text_member(document, id, "guest_target")?; - let host_path = text_member(document, id, "host_path")?; - let access = text_member(document, id, "access")?; - let denied = paths_member(document, id, "denied_paths")?; - // Configurations written before ownership modes ran shares as the VMM. - let owner = match member(document, id, "owner") { - Some(owner) => py_str(document, owner).ok_or(Malformed::Malformed)?, - None => "vmm".to_owned(), - }; - let allowed = if policy { - paths_member(document, id, "allowed_paths")? - } else { - Vec::new() - }; - let writable = if policy { - paths_member(document, id, "writable_paths")? - } else { - Vec::new() - }; - let kind = if kind { - text_member(document, id, "kind")? - } else { - "directory".to_owned() - }; - validate_mount_target(&guest_target)?; - let access = MountAccess::from_name(&access)?; - let owner = MountOwner::from_name(&owner)?; - let kind = MountKind::from_name(&kind)?; - let policy = MountPolicy { - denied, - allowed, - writable, - }; - Ok(SandboxMount::new( - &guest_target, - &host_path, - access, - policy, - owner, - kind, - )?) -} - -/// Reads the shares of a recorded configuration, as `_deserialize_mounts` does. -fn deserialize_mounts( +/// Reads the export plan of the shares that a configuration records under `export`, in its serde +/// form, or `None` for a configuration without shares, whose `mount` is `null` or missing. +/// +/// A configuration with a share under `mount`, which only formats that `start` no longer reads +/// recorded, reads as one with shares but no plan, which [`deserialize_launch`] refuses. +fn deserialize_export( document: &Document, root: NodeId, -) -> std::result::Result, Malformed> { - let format = member(document, root, "format"); - let is_format = |expected| format.is_some_and(|format| equals_int(document, format, expected)); - let mounts_format = [ - MULTI_MOUNT_CONFIG_FORMAT, - POLICY_CONFIG_FORMAT, - FILE_MOUNT_CONFIG_FORMAT, - ] - .into_iter() - .find(|&format| is_format(format)); - if let Some(format) = mounts_format { - let mounts = member(document, root, "mounts").ok_or(Malformed::Malformed)?; - let Node::Array(items) = document.node(mounts) else { - return Err(Malformed::Malformed); - }; - return items - .iter() - .map(|&item| { - deserialize_mount( - document, - item, - format != MULTI_MOUNT_CONFIG_FORMAT, - format == FILE_MOUNT_CONFIG_FORMAT, - ) - }) - .collect(); - } - match member(document, root, "mount") { - None => Ok(Vec::new()), - Some(mount) if matches!(document.node(mount), Node::Null) => Ok(Vec::new()), - Some(mount) => Ok(vec![deserialize_mount(document, mount, false, false)?]), +) -> std::result::Result<(bool, Option), Malformed> { + if let Some(export) = member(document, root, EXPORT) { + let export = json_value(document, export, MAX_EXPORT_DEPTH) + .and_then(|value| serde_json::from_value(value).ok()) + .ok_or(Malformed::Malformed)?; + return Ok((true, Some(export))); } + let shares = member(document, root, "mount") + .is_some_and(|mount| !matches!(document.node(mount), Node::Null)); + Ok((shares, None)) } /// Reads the launch that a configuration records, as `_deserialize_launch` does, and checks it /// against the host. /// /// A missing field, or a value of the wrong type, fails as `sandbox configuration is malformed`, -/// as does a layer whose role is not `distro`. A configuration whose format is not the one that -/// its shares need fails as `sandbox configuration format does not match its mounts`. +/// as does a layer whose role is not `distro` or an export plan that is not of its serde form. A +/// configuration whose format is not the one that its shares need fails as `sandbox configuration +/// format does not match its mounts`. The export plan must then pass the checks of +/// [`SandboxLaunch::with_export`], and the launch those of [`SandboxLaunch::validated`], which +/// checks again the host objects that the plan recorded. pub(crate) fn deserialize_launch(document: &Document) -> Result { let malformed = || Exit::error("sandbox configuration is malformed"); - let launch = (|| -> std::result::Result { + let (launch, shares, export) = (|| -> std::result::Result<_, Malformed> { let root = document.root(); let layers_value = member(document, root, "layers").ok_or(Malformed::Malformed)?; let mut layers = Vec::new(); @@ -763,7 +597,7 @@ pub(crate) fn deserialize_launch(document: &Document) -> Result { }; let memory_max = limit("memory_max")?; let pids_max = limit("pids_max")?; - let mounts = deserialize_mounts(document, root)?; + let (shares, export) = deserialize_export(document, root)?; let launch = SandboxLaunch::new( layers, &scratch, @@ -772,22 +606,29 @@ pub(crate) fn deserialize_launch(document: &Document) -> Result { workload_identity, memory_max, pids_max, - mounts, ..SandboxSettings::default() }, )?; - Ok(launch) + Ok((launch, shares, export)) })() .map_err(|error| match error { Malformed::Malformed => malformed(), Malformed::Exit(exit) => exit, })?; let format = member(document, document.root(), "format"); - if !format.is_some_and(|format| equals_int(document, format, config_format(launch.mounts()))) { + let expected = if shares { + EXPORT_CONFIG_FORMAT + } else { + CONFIG_FORMAT + }; + if (shares && export.is_none()) + || !format.is_some_and(|format| equals_int(document, format, expected)) + { return Err(Exit::error( "sandbox configuration format does not match its mounts", )); } + let launch = launch.with_export(export)?; launch.validated()?; Ok(launch) } diff --git a/nvx-rs/src/host/cli/sandbox/tests.rs b/nvx-rs/src/host/cli/sandbox/tests.rs new file mode 100644 index 000000000..c71cb5dc0 --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/tests.rs @@ -0,0 +1,59 @@ +//! Tests of the checks of a one-shot run. + +use std::fs; + +use super::*; +use crate::host::python::fs::resolve; +use crate::host::sandbox::layer::LayerRole; + +#[test] +fn run_checks_again_that_its_shares_do_not_overlap() { + let directory = tempfile::tempdir().unwrap(); + let root = resolve(&PurePath::native(directory.path().to_str().unwrap())).unwrap(); + let path = |name: &str| root.join(name).as_str().to_owned(); + for (name, content) in [ + ("distro.erofs", "distro"), + ("scratch.ext4", "scratch"), + ("settings.json", "{}"), + ("output.log", ""), + ] { + fs::write(path(name), content).unwrap(); + } + let mounts = sandbox_mounts( + &[ + format!("/etc/settings.json,{}", path("settings.json")), + format!("/var/log/output.log,{},rw", path("output.log")), + ], + &[], + None, + ) + .unwrap(); + validate_mounts(&mounts).unwrap(); + let launch = SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + &path("distro.erofs"), + "11111111-1111-1111-1111-111111111111", + )], + &path("scratch.ext4"), + SandboxSettings::default(), + ) + .unwrap() + .with_export(plan_mounts(&mounts).unwrap()) + .unwrap(); + let binding = validate_sandbox_systemd_policy(&launch).unwrap(); + run_plan(&launch, &binding).unwrap(); + + // A hard link that replaced the read-only file with the writable one while the run verified + // its distro layer would export one file under both access modes; the export plan's own + // checks pass, since a file share's root is not pinned. + fs::remove_file(path("settings.json")).unwrap(); + fs::hard_link(path("output.log"), path("settings.json")).unwrap(); + launch.export().unwrap().verify().unwrap(); + let error = run_plan(&launch, &binding).unwrap_err(); + let message = error.message().unwrap_or_default(); + assert!( + message.contains("name the same directory or file"), + "{message}" + ); +} diff --git a/nvx-rs/src/host/filesystem.rs b/nvx-rs/src/host/filesystem.rs index 77a5b1fec..1e35d4a0f 100644 --- a/nvx-rs/src/host/filesystem.rs +++ b/nvx-rs/src/host/filesystem.rs @@ -1,27 +1,39 @@ -//! Host paths exposed to the guest through OpenVMM's aggregate virtio-fs export. +//! Host paths exposed to the guest through OpenVMM's aggregate virtio-fs export, as the one +//! planner of every NVX caller plans them, for the blockless root and for the container root of a +//! layered sandbox alike. //! -//! OpenVMM offers one virtio-fs device. NVX attaches it as an aggregate: a synthetic, +//! OpenVMM offers one virtio-fs device. [`plan`] attaches it as an aggregate: a synthetic, //! read-only root that lists one child per exported host directory or file, named by its index, -//! which the guest's init mounts at a directory that only the guest's root can enter. The guest -//! agent then bind-mounts each mapped path at its target, read-only or read-write, from the -//! mapping table that it receives over the control channel. +//! which the guest mounts at a directory that only the guest's root can enter. The guest then +//! bind-mounts each mapped path at its target, read-only or read-write, in the order of the plan's +//! [`Bind`]s: from the mapping table that it receives over the control channel, or from boot +//! tokens that a caller renders from the same binds. +//! +//! An [`ExportRequest`] names the mapped host paths, each read-only or read-write at its guest +//! target; the host paths that OpenVMM hides inside them, and the paths inside those that stay +//! visible; the paths to which the writes of a read-write directory are narrowed; the host +//! identity of the file operations; and the guest targets that the guest's root keeps for itself. +//! +//! - A caller gives each target, or lets MXC's convention for Linux guests derive it from the +//! resolved host path: a Windows path `C:\work\src` appears at `/mnt/c/work/src`, and a Linux +//! path at the same path ([`guest_path`]). +//! - The reserved targets depend on the root, and the caller supplies them ([`ReservedTargets`]): +//! the blockless root keeps the guest's system trees, and the container root of a layered +//! sandbox keeps the trees of its runtime. //! //! The children are the outermost mapped directories and the mapped files outside them. OpenVMM //! exports each such file alone, without its directory, so mapped paths may lie anywhere, on any //! volume, except that a whole volume cannot be mapped. A child is read-write only if it holds a -//! read-write mapping, and OpenVMM then limits writes to its read-write mappings, so the host +//! read-write mapping, and OpenVMM then limits writes to the child's writable parts, so the host //! enforces each mapping's access itself, and guest mount flags are never the boundary. OpenVMM -//! cannot keep a path read-only inside a writable one, so a read-only path inside a read-write -//! directory is refused, while a read-write path inside a read-only directory is not. OpenVMM -//! also hides the denied paths. +//! cannot keep a path read-only inside a writable part, so a read-only path there is refused on +//! every host, while a read-write path inside a read-only directory is not. OpenVMM also hides the +//! denied paths, except for the allowed paths inside them. //! +//! The plan records the identities of the mapped and denied paths that a workload could move, +//! those inside a writable part, and [`ExportPlan::verify`] checks them again before each start. //! A policy that would need more children than OpenVMM offers is refused rather than served -//! through the directories of its files, which would grant more than the files. Records of -//! earlier crates may still hold such a directory as a hidden child, whose root is no mapping -//! itself and which exposes only its mapped paths. -//! -//! Guest targets follow MXC's convention for Linux guests: a Windows path `C:\work\src` appears at -//! `/mnt/c/work/src`, and a Linux path appears at the same path. +//! through the directories of its files, which would grant more than the files. use std::fs; use std::path::{Component, Path, PathBuf, Prefix}; @@ -30,6 +42,7 @@ use serde::{Deserialize, Serialize}; use super::platform; use crate::error::{Error, Result}; +use crate::protocol::share::{RESERVED_EXACT_MOUNT_TARGETS, RESERVED_MOUNT_TARGETS}; use crate::protocol::{HostMap, MAPS_TOKEN, MAX_HOST_MAPPINGS}; /// Guest directory where the guest's init mounts the export; only the guest's root can enter @@ -39,34 +52,111 @@ pub const GUEST_EXPORT: &str = "/run/nvx/hostfs/root"; /// Largest number of host directories and files that OpenVMM exports through one aggregate. pub const MAX_CHILDREN: usize = 256; /// Largest number of hidden, allowed, or writable paths that OpenVMM accepts in one exported -/// directory, and their largest combined length. +/// directory. const MAX_POLICY_PATHS: usize = 128; -/// Largest combined UTF-8 byte length of one exported directory's policy paths. +/// Largest combined UTF-8 byte length of one exported directory's paths of one kind. const MAX_POLICY_BYTES: usize = 16 * 1024; -/// Largest length of one such path, and of all of them together. +/// Largest length of one such path. const MAX_POLICY_PATH_BYTES: usize = 4096; /// Largest combined UTF-8 byte length of all policies in one aggregate export. const MAX_AGGREGATE_POLICY_BYTES: usize = 128 * 1024; /// Longest guest path, without the terminating NUL. const MAX_GUEST_PATH: usize = 4095; -/// Guest directories that a mapping must not cover, because the guest's own system lives there. +/// Guest directories that a mapping must not cover in the blockless root, because the guest's own +/// system lives there. pub const RESERVED_TARGETS: [&str; 13] = [ "/bin", "/boot", "/dev", "/etc", "/lib", "/proc", "/root", "/run", "/sbin", "/sys", "/usr", "/var", "/init", ]; +/// Why a request for the caller owner fails on a Windows host. +const CALLER_OWNER_ON_WINDOWS: &str = "the caller owner requires a Linux host; Windows has no \ + per-request POSIX identity for OpenVMM to switch to"; + +/// The guest targets that a root keeps for its own system or runtime, which no mapping may take. +/// +/// A mapping may be neither at nor below one of the [`trees`](Self::trees), nor at one of their +/// ancestors, where its bind would shadow the tree, nor at one of the [`exact`](Self::exact) +/// paths, though it may be below them. No mapping is ever at the guest's root, `/`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ReservedTargets<'a> { + /// Guest trees that the root owns. + pub trees: &'a [&'a str], + /// Guest paths at which the root mounts or creates something, below which mappings may lie. + pub exact: &'a [&'a str], +} + +impl ReservedTargets<'static> { + /// The blockless root, in which the guest's own system runs: [`RESERVED_TARGETS`]. + pub const BLOCKLESS_ROOT: Self = Self { + trees: &RESERVED_TARGETS, + exact: &[], + }; + + /// The container root of a layered sandbox: the trees in which its runtime mounts, binds, or + /// stages something, [`RESERVED_MOUNT_TARGETS`], and exactly the directories on the way to + /// them that it creates, [`RESERVED_EXACT_MOUNT_TARGETS`], which the guest checks as well. + pub const CONTAINER_ROOT: Self = Self { + trees: &RESERVED_MOUNT_TARGETS, + exact: &RESERVED_EXACT_MOUNT_TARGETS, + }; +} + +impl<'a> ReservedTargets<'a> { + /// Returns the reserved path that a mapping at `target`, an absolute guest path, would take + /// or shadow, or `None` if `target` is free. + pub fn reservation(&self, target: &str) -> Option<&'a str> { + if target == "/" { + return Some("/"); + } + self.exact + .iter() + .copied() + .find(|exact| *exact == target) + .or_else(|| { + self.trees + .iter() + .copied() + .find(|tree| within(target, tree) || within(tree, target)) + }) + } +} + +/// The host identity under which OpenVMM runs the file operations of an export. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Owner { + /// Every operation runs as OpenVMM, the default. + #[default] + Vmm, + /// Each operation runs as the identity of the guest caller, with guest root squashed to the + /// owner of the exported directory or file. Linux hosts only. + Caller, +} + +impl Owner { + /// Returns whether this is the default owner, which the serde form of a plan leaves out. + fn is_vmm(&self) -> bool { + *self == Self::Vmm + } +} + /// The host paths of one sandbox, as [`plan`] exports them: the children of OpenVMM's aggregate -/// export and the bind mounts of the guest agent. +/// export, the bind mounts of the guest, and the host identity of the file operations. /// -/// Callers record the plan, so its serde form is part of their state and must not change. +/// Callers record the plan, so its serde form is part of their state: a caller versions its +/// records, and a record of another version is not read. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ExportPlan { /// Children of the aggregate export; each is named by its index. pub children: Vec, - /// Bind mounts the guest agent creates, parents before children. + /// Bind mounts the guest creates, parents before children. pub binds: Vec, + /// The host identity of the file operations; a plan of the default owner leaves it out. + #[serde(default, skip_serializing_if = "Owner::is_vmm")] + pub owner: Owner, } /// One host directory or file of the aggregate export. @@ -75,21 +165,19 @@ pub struct ExportPlan { pub struct ExportChild { /// Canonical host directory, or the canonical host file that the child exposes alone. pub root: PathBuf, - /// Whether the root is a regular file, which OpenVMM exposes without its directory. Records - /// without file children leave it out, and earlier crates refuse records with them. + /// Whether the root is a regular file, which OpenVMM exposes without its directory; a child + /// of a directory leaves it out. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub file: bool, /// Whether the guest may modify it, which a read-write mapping inside it requires. pub writable: bool, - /// Whether OpenVMM hides everything but the allowed paths, because the root is no mapping, - /// which only records of earlier crates hold. - pub hidden: bool, - /// Canonical host paths inside the child that OpenVMM hides. + /// Canonical host paths inside the child that OpenVMM hides; the root itself, when it shows + /// only its allowed paths. pub denied: Vec, - /// Identities, at provision, of the denied paths that a workload could move: those inside - /// a read-write mapping. + /// Identities, at provision, of the denied paths that a workload could move: those inside a + /// writable part. pub denied_identities: Vec>, - /// Canonical mapped paths that stay visible below a hidden root. + /// Canonical paths inside denied paths that stay visible. pub allowed: Vec, /// Canonical paths to which OpenVMM limits the guest's writes; empty if the guest may modify /// everything that it can reach in a writable child. @@ -118,7 +206,7 @@ pub struct Bind { pub target: String, /// Whether the guest mounts it read-only. pub read_only: bool, - /// Identity of the host object at provision, when it lies inside a read-write mapping and a + /// Identity of the host object at provision, when it lies inside a writable part and a /// workload could therefore move it. #[serde(default, skip_serializing_if = "Option::is_none")] pub identity: Option, @@ -193,6 +281,43 @@ pub fn resolve_guest_path(path: &Path) -> Option { } } +/// Where the guest sees a mapped host path. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Target<'a> { + /// At the guest path that [`guest_path`] derives from the resolved host path. + Convention, + /// At this absolute guest path: a path other than the root, of at most 4,095 bytes, without + /// NUL characters or empty, `.`, or `..` components. + Guest(&'a str), +} + +/// A host directory or regular file that the guest reads, or reads and writes, at a target. +#[derive(Debug, Clone, Copy)] +pub struct Mapping<'a> { + /// How messages name the list or option that requested the mapping, such as + /// `filesystem.readonlyPaths`. + pub name: &'a str, + /// The host path. + pub path: &'a Path, + /// Where the guest sees it. + pub target: Target<'a>, + /// Whether the guest may only read it. + pub read_only: bool, +} + +impl<'a> Mapping<'a> { + /// Returns the mappings of the paths of `list`, under its name, at the guest paths of the + /// convention, read-only or read-write. + pub fn convention(list: PathList<'a>, read_only: bool) -> impl Iterator + 'a { + list.paths.iter().map(move |path| Self { + name: list.name, + path, + target: Target::Convention, + read_only, + }) + } +} + /// A list of host paths, and the name by which messages refer to its entries, such as /// `readOnly`. #[derive(Debug, Clone, Copy)] @@ -203,84 +328,99 @@ pub struct PathList<'a> { pub paths: &'a [PathBuf], } -/// The host paths that a sandbox maps, read-only or read-write, and those that it hides inside -/// them. +impl PathList<'static> { + /// A list without paths. + pub const EMPTY: Self = Self { + name: "", + paths: &[], + }; +} + +/// The host paths that a sandbox maps, those that it hides inside them and shows again inside +/// those, the paths to which it narrows writes, the host identity of the file operations, and the +/// guest targets that the guest's root keeps for itself. #[derive(Debug, Clone, Copy)] pub struct ExportRequest<'a> { - /// Host paths that the guest reads. - pub read_only: PathList<'a>, - /// Host paths that the guest reads and writes. - pub read_write: PathList<'a>, - /// Host paths hidden inside the mapped paths. + /// The mapped host paths, each at its target with its access. + pub mappings: &'a [Mapping<'a>], + /// Host paths hidden inside the mapped paths. A mapped directory may be denied itself when + /// allowed paths inside it stay visible. pub denied: PathList<'a>, + /// Host paths inside denied paths that stay visible, with the directories on the way to + /// them listing only them. + pub allowed: PathList<'a>, + /// Host paths to which the guest's writes are narrowed inside the read-write directory that + /// they lie in, which must be mapped whole: the guest may then write only them and the + /// read-write mappings inside that directory. + pub write: PathList<'a>, + /// The host identity of the file operations. + pub owner: Owner, + /// The guest targets that the guest's root keeps for itself. + pub reserved: ReservedTargets<'a>, +} + +impl<'a> ExportRequest<'a> { + /// Returns the request that maps `mappings` into a root that reserves `reserved`, without + /// denied, allowed, or write paths, as the VMM. + pub fn new(mappings: &'a [Mapping<'a>], reserved: ReservedTargets<'a>) -> Self { + Self { + mappings, + denied: PathList::EMPTY, + allowed: PathList::EMPTY, + write: PathList::EMPTY, + owner: Owner::Vmm, + reserved, + } + } } /// Plans the export of `request`, or returns `None` if it maps nothing. /// -/// Mapped paths must exist, and their guest paths derive from their resolved host paths, so two -/// spellings of one object (links, letter case, or short names) map to one guest path. A path -/// that is both read-only and read-write is mapped read-only. A read-only path must not lie -/// inside a read-write directory, where the host could not keep it read-only, but a read-write -/// path may lie inside a read-only directory. A whole volume cannot be mapped, and a policy that -/// would need more children than OpenVMM offers is refused. A denied path must not contain a -/// mapped path, and a denied path that does not exist yet must not lie inside a mapped path, -/// because nothing could hide it once a workload creates it. +/// Mapped paths must exist, and resolve to paths that are valid UTF-8. A mapping's target is the +/// one that the request gives, or the guest path that the convention derives from the resolved +/// host path, so two spellings of one object (links, letter case, or short names) map to one guest +/// path; either must be free in the request's [`ReservedTargets`]. An object mapped twice at one +/// target is mapped once, read-only if either mapping is. Two objects never share a target, and a +/// target inside another mapping's must show the very object that the other mapping shows there. +/// A whole volume cannot be mapped, and a policy that would need more children than OpenVMM +/// offers is refused. +/// +/// A read-only path must not lie inside a writable part, where the host could not keep it +/// read-only: inside a read-write directory, unless write paths narrow that directory's writes +/// to other paths, or inside a write path. A read-write path may lie inside a read-only +/// directory. A write path must lie inside a read-write directory that a child exports whole. +/// +/// A denied path must not hide a mapped path, except that a denied mapped directory shows the +/// allowed paths inside it, unless it is a read-write mapping inside a directory that is not +/// writable as a whole: OpenVMM narrows that directory's writes to the mapping, and it cannot make +/// a hidden path writable. A denied path that does not exist yet must not lie inside a mapped +/// path, because nothing could hide it once a workload creates it; denied paths outside the +/// mapped paths are ignored. An allowed path must lie inside a denied path, with no other allowed +/// path between them, and a write path must not be hidden. The caller owner needs a Linux host, +/// on which no exported directory or file may belong to UID 0 or GID 0. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when the request breaks /// these rules or exceeds a limit of OpenVMM or the guest agent. pub fn plan(request: &ExportRequest<'_>) -> Result> { let unsupported = |message: String| Err(Error::unsupported(message)); - let mut mapped: Vec = Vec::new(); - for (list, read_only) in [(request.read_only, true), (request.read_write, false)] { - let field = list.name; - for path in list.paths { - let canonical = canonicalize(path).map_err(|error| { - Error::unsupported(format!( - "{field} entry {} is not accessible", - path.display() - )) - .with_source(error) - })?; - let metadata = fs::metadata(&canonical).map_err(|error| { - Error::unsupported(format!("cannot inspect {}", path.display())).with_source(error) - })?; - if !metadata.is_dir() && !metadata.is_file() { - return unsupported(format!( - "{field} entry {} is neither a directory nor a regular file", - path.display() - )); - } - if metadata.is_dir() - && let Some(reason) = volume_root_refusal(&canonical) - { - return unsupported(format!("{field} entry {} {reason}", path.display())); - } - let Some(target) = guest_path(&canonical) else { - return unsupported(format!( - "{field} entry {} has no guest path; use an absolute local path", - path.display() - )); - }; - if let Some(reserved) = RESERVED_TARGETS - .iter() - .find(|reserved| target == "/" || within(&target, reserved)) - { - return unsupported(format!( - "{field} entry {} would cover the guest's {reserved}", - path.display() - )); - } - match mapped.iter_mut().find(|other| other.canonical == canonical) { - // The same object listed twice keeps its most restrictive access. - Some(other) => other.read_only |= read_only, - None => mapped.push(Mapped { - given: path.clone(), - canonical, - target, - read_only, - directory: metadata.is_dir(), - }), + if request.owner == Owner::Caller && cfg!(windows) { + return unsupported(CALLER_OWNER_ON_WINDOWS.to_owned()); + } + let mut mapped: Vec> = Vec::new(); + for mapping in request.mappings { + let entry = resolve_mapping(mapping, request.reserved)?; + match mapped + .iter_mut() + .find(|other| other.canonical == entry.canonical && other.target == entry.target) + { + // The same object listed twice at one target keeps its most restrictive access, and + // the name and spelling of the entry that gave it. + Some(other) => { + if entry.read_only && !other.read_only { + *other = entry; + } } + None => mapped.push(entry), } } if mapped.is_empty() { @@ -291,39 +431,11 @@ pub fn plan(request: &ExportRequest<'_>) -> Result> { "the openvmm backend maps at most {MAX_HOST_MAPPINGS} paths into one sandbox" )); } - // OpenVMM can limit a child's writes to some of its paths, but cannot keep a path inside them - // read-only. Only the guest's bind mount would then protect a read-only path inside a - // read-write directory, and guest root can bypass it: through the export, or on a Windows - // host, which opens files case-insensitively, through another spelling of a file's name. - // Each read-only entry's given path comes from the read-only list, and each read-write - // entry's from the read-write list, because the read-only list is merged first. - if let Some((inner, outer)) = mapped - .iter() - .filter(|entry| entry.read_only) - .find_map(|inner| { - mapped - .iter() - .find(|outer| { - outer.directory - && !outer.read_only - && inner.canonical != outer.canonical - && inner.canonical.starts_with(&outer.canonical) - }) - .map(|outer| (inner, outer)) - }) - { - return unsupported(format!( - "{} entry {} lies inside the {} entry {}, where the host cannot keep it read-only; \ - instead, map that directory read-only and the paths in it that the guest writes \ - read-write", - request.read_only.name, - inner.given.display(), - request.read_write.name, - outer.given.display() - )); - } - - let roots = export_roots(&mapped)?; + check_targets(&mapped)?; + let roots = outermost(mapped.iter().map(|entry| entry.canonical.clone()).collect()); + let write = write_paths(request.write, &mapped, &roots)?; + check_read_only(&mapped, &write, request.write.name, &roots)?; + distinct_roots(&roots)?; if roots.len() > MAX_CHILDREN { // Exporting directories for some of the files would grant more than the files. return unsupported(format!( @@ -332,70 +444,96 @@ pub fn plan(request: &ExportRequest<'_>) -> Result> { roots.len() )); } - let denied = denied_paths(request.denied, &mapped)?; - // Only objects inside a read-write mapping can be swapped by a workload; pinning others - // would make ordinary host edits, which often replace files, block the next start. + let allowed = allowed_paths(request.allowed, &roots)?; + let denied = denied_paths(request.denied, &mapped, &allowed)?; + check_allowed_paths(request.allowed.name, request.denied.name, &allowed, &denied)?; + check_write_paths( + request.write.name, + request.denied.name, + &write, + &denied, + &allowed, + )?; + let denied = canonicals(&denied); + let mut allowed = canonicals(&allowed); + allowed.sort(); + + let parts: Vec = roots + .iter() + .map(|root| writable_part(root, &mapped, &write)) + .collect(); + // A read-write mapping inside a child that is not writable as a whole becomes one of the + // child's write paths, and OpenVMM refuses to hide a write path. The write paths of the + // request were checked already. + for (root, part) in roots.iter().zip(&parts) { + for path in &part.write { + if let Some((rule, true)) = nearest_rule(path, &denied, &allowed, true) { + let (name, given) = mapped + .iter() + .find(|entry| entry.canonical == *path) + .map_or((request.write.name, path.as_path()), |entry| { + (entry.name, entry.given.as_path()) + }); + return unsupported(format!( + "{} entry {} hides the {name} entry {}, to which OpenVMM narrows the writes \ + of {}, and OpenVMM cannot make a hidden path writable; map read-write the \ + paths that it shows again instead", + request.denied.name, + rule.display(), + given.display(), + root.display() + )); + } + } + } + // Only objects inside a writable part can be swapped by a workload; pinning others would + // make ordinary host edits, which often replace files, block the next start. let movable = |path: &Path| { - mapped.iter().any(|entry| { - entry.directory - && !entry.read_only - && path != entry.canonical - && path.starts_with(&entry.canonical) - }) + roots + .iter() + .zip(&parts) + .any(|(root, part)| path.starts_with(root) && part.moves(root, path)) }; - let mut children = Vec::with_capacity(roots.len()); let mut policy_bytes = 0; - for root in roots { - let inside: Vec<&Mapped> = mapped - .iter() - .filter(|entry| entry.canonical.starts_with(&root)) - .collect(); + for (root, part) in roots.iter().zip(&parts) { // Every root is a mapping itself, so the guest reaches all of it. - let Some(own) = inside.iter().find(|entry| entry.canonical == root) else { + let Some(own) = mapped.iter().find(|entry| entry.canonical == *root) else { return Err(Error::other(format!( "{} is exported but not mapped", root.display() ))); }; - let read_write: Vec = inside - .iter() - .filter(|entry| !entry.read_only) - .map(|entry| entry.canonical.clone()) - .collect(); - let writable = !read_write.is_empty(); - // Unless the read-write mappings cover the whole child, OpenVMM limits writes to them. - let write = if writable && own.read_only { - outermost(read_write) - } else { - Vec::new() + let inside = |paths: &[PathBuf]| -> Vec { + paths + .iter() + .filter(|path| path.starts_with(root)) + .cloned() + .collect() }; - let child_denied: Vec = denied - .iter() - .filter(|path| path.starts_with(&root)) - .cloned() - .collect(); - check_root(&root)?; - policy_bytes += check_policy_paths(&root, &child_denied, "hide")?; - policy_bytes += check_policy_paths(&root, &write, "make writable")?; + let child_allowed = inside(&allowed); + let child_denied = needed_denied_paths(&inside(&denied), &child_allowed); + check_root(root)?; + policy_bytes += check_policy_paths(root, &child_denied, "hide")?; + policy_bytes += check_policy_paths(root, &child_allowed, "show again")?; + policy_bytes += check_policy_paths(root, &part.write, "make writable")?; let denied_identities = child_denied .iter() .map(|path| movable(path).then(|| identity(path)).transpose()) .collect::>>()?; children.push(ExportChild { - root, + root: root.clone(), file: !own.directory, - writable, - hidden: false, + writable: part.writable, denied: child_denied, denied_identities, - allowed: Vec::new(), - write, + allowed: child_allowed, + write: part.write.clone(), }); } if policy_bytes > MAX_AGGREGATE_POLICY_BYTES { return unsupported(format!( - "the paths that OpenVMM is to hide or make writable exceed its \ + "the paths that OpenVMM is to hide, show again, or make writable exceed its \ {MAX_AGGREGATE_POLICY_BYTES}-byte limit" )); } @@ -437,29 +575,317 @@ pub fn plan(request: &ExportRequest<'_>) -> Result> { .cmp(&depth(&right.target)) .then_with(|| left.target.cmp(&right.target)) }); - Ok(Some(ExportPlan { children, binds })) + if request.owner == Owner::Caller { + check_caller_roots(&children)?; + } + let plan = ExportPlan { + children, + binds, + owner: request.owner, + }; + // Callers read a record of the plan back through `check`, which accepts every plan that this + // function makes. + debug_assert!( + plan.check(request.reserved).is_ok(), + "the planner made a plan that its check refuses: {:?}", + plan.check(request.reserved) + ); + Ok(Some(plan)) } -/// Explains why a mapped host directory, given in canonical form, cannot be exported, or returns -/// `None` if it can: a whole volume. It inspects only the path, not the host. -fn volume_root_refusal(canonical: &Path) -> Option<&'static str> { - (!canonical - .components() - .any(|component| matches!(component, Component::Normal(_)))) - .then_some( - "is a whole volume, which the openvmm backend does not map; map the directories inside \ - it", - ) +/// Resolves one requested mapping: its host path must exist as a directory or regular file that +/// is not a whole volume, and its target must be a guest path that `reserved` leaves free. +fn resolve_mapping<'a>(mapping: &Mapping<'a>, reserved: ReservedTargets<'_>) -> Result> { + let unsupported = |message: String| Err(Error::unsupported(message)); + let Mapping { + name, + path, + target, + read_only, + } = *mapping; + let canonical = canonicalize(path).map_err(|error| { + Error::unsupported(format!("{name} entry {} is not accessible", path.display())) + .with_source(error) + })?; + // OpenVMM's options and the records of a plan carry host paths as text. + if canonical.to_str().is_none() { + return unsupported(format!( + "{name} entry {} resolves to a path that is not valid UTF-8", + path.display() + )); + } + let metadata = fs::metadata(&canonical).map_err(|error| { + Error::unsupported(format!("cannot inspect {}", path.display())).with_source(error) + })?; + if !metadata.is_dir() && !metadata.is_file() { + return unsupported(format!( + "{name} entry {} is neither a directory nor a regular file", + path.display() + )); + } + if metadata.is_dir() + && let Some(reason) = volume_root_refusal(&canonical) + { + return unsupported(format!("{name} entry {} {reason}", path.display())); + } + let target = match target { + Target::Convention => { + let Some(target) = guest_path(&canonical) else { + return unsupported(format!( + "{name} entry {} has no guest path; use an absolute local path", + path.display() + )); + }; + if let Some(reserved) = reserved.reservation(&target) { + return unsupported(format!( + "{name} entry {} would cover the guest's {reserved}", + path.display() + )); + } + target + } + Target::Guest(target) => { + if let Some(problem) = target_problem(target) { + return unsupported(format!( + "{name} entry {} has the guest target {target:?}, which {problem}", + path.display() + )); + } + if let Some(reserved) = reserved.reservation(target) { + return unsupported(format!( + "{name} entry {} has the guest target {target}, which would cover the \ + guest's {reserved}", + path.display() + )); + } + target.to_owned() + } + }; + Ok(Mapped { + name, + given: path.to_path_buf(), + canonical, + target, + read_only, + directory: metadata.is_dir(), + }) } -/// Returns the paths to export, in order: the outermost mapped directories and the mapped files -/// outside them, each exported alone. Distinct paths must not name one directory or file. -fn export_roots(mapped: &[Mapped]) -> Result> { - let roots = outermost(mapped.iter().map(|entry| entry.canonical.clone()).collect()); - // OpenVMM refuses one object exported twice, which bind mounts and hard links make - // possible. +/// Explains why `target` cannot be a guest target, or returns `None` if it can: it must be an +/// absolute path other than the root, of at most [`MAX_GUEST_PATH`] bytes, without NUL characters +/// or empty, `.`, or `..` components. +fn target_problem(target: &str) -> Option<&'static str> { + let Some(rest) = target.strip_prefix('/') else { + return Some("is not absolute"); + }; + if rest.is_empty() { + return Some("is the guest's root"); + } + if target.len() > MAX_GUEST_PATH { + return Some("is longer than a guest path may be"); + } + if target.contains('\0') { + return Some("contains a NUL character"); + } + if rest + .split('/') + .any(|component| matches!(component, "" | "." | "..")) + { + return Some("has an empty, `.`, or `..` component"); + } + None +} + +/// Refuses mappings whose targets collide: two objects at one target, or a target inside another +/// mapping's that does not show the very object that the other mapping shows there, which it +/// would hide. +fn check_targets(mapped: &[Mapped<'_>]) -> Result<()> { + for (index, entry) in mapped.iter().enumerate() { + for other in &mapped[..index] { + // The entries of one object at one target are merged already. + if entry.target == other.target { + return Err(Error::unsupported(format!( + "{} entry {} and the {} entry {} have the same guest target {}", + entry.name, + entry.given.display(), + other.name, + other.given.display(), + entry.target + ))); + } + let (outer, inner) = if within(&entry.target, &other.target) { + (other, entry) + } else if within(&other.target, &entry.target) { + (entry, other) + } else { + continue; + }; + if !shows(outer, inner) { + return Err(Error::unsupported(format!( + "{} entry {} at {} would hide part of the {} entry {} at {}; map it at a \ + target outside that one", + inner.name, + inner.given.display(), + inner.target, + outer.name, + outer.given.display(), + outer.target + ))); + } + } + } + Ok(()) +} + +/// Returns whether `inner`, whose target lies below that of `outer`, maps the host object that +/// `outer` shows at that target: whether the rest of `inner`'s target spells, name for name, the +/// host path from `outer`'s object to `inner`'s, as a plan's bind sources spell it. A Windows host +/// reads a backslash in a name as a separator, and a name with a root or a drive as another path, +/// so such a guest name never matches. +fn shows(outer: &Mapped<'_>, inner: &Mapped<'_>) -> bool { + let Some(rest) = inner + .target + .strip_prefix(outer.target.as_str()) + .and_then(|rest| rest.strip_prefix('/')) + else { + return false; + }; + outer.directory + && relative_text(&outer.canonical, &inner.canonical).is_some_and(|names| names == rest) +} + +/// A path of an access policy, resolved. +struct PolicyPath { + /// Host path as the caller supplied it, for diagnostics. + given: PathBuf, + /// Link-resolved host path. + canonical: PathBuf, +} + +/// Returns the resolved paths of `paths`. +fn canonicals(paths: &[PolicyPath]) -> Vec { + paths.iter().map(|path| path.canonical.clone()).collect() +} + +/// Resolves the path of the list `field`, which must exist. +fn resolve_policy_path(field: &str, path: &Path) -> Result { + canonicalize(path).map_err(|error| { + Error::unsupported(format!( + "{field} entry {} is not accessible", + path.display() + )) + .with_source(error) + }) +} + +/// Resolves the write paths of `list`: each must lie inside a read-write directory that a child +/// of `roots` exports whole, other than that directory itself. +fn write_paths( + list: PathList<'_>, + mapped: &[Mapped<'_>], + roots: &[PathBuf], +) -> Result> { + let field = list.name; + let mut paths = Vec::with_capacity(list.paths.len()); + for path in list.paths { + let canonical = resolve_policy_path(field, path)?; + let narrows = roots + .iter() + .find(|root| canonical.starts_with(root)) + .is_some_and(|root| { + *root != canonical + && mapped.iter().any(|entry| { + entry.canonical == *root && entry.directory && !entry.read_only + }) + }); + if !narrows { + return Err(Error::unsupported(format!( + "{field} entry {} lies in no directory that is mapped read-write as a whole, \ + whose writes it could narrow", + path.display() + ))); + } + paths.push(PolicyPath { + given: path.clone(), + canonical, + }); + } + Ok(paths) +} + +/// Refuses a read-only mapping that the host could not keep read-only: one inside a write path, +/// one that a read-write mapping holds, unless write paths narrow that mapping's writes, or one of +/// an object that a read-write mapping maps elsewhere. OpenVMM can limit a child's writes to some +/// of its paths, but cannot keep a path inside them read-only. Only the guest's bind mount would +/// then protect the path, and guest root can bypass it: through the export, or on a Windows host, +/// which opens files case-insensitively, through another spelling of a file's name. +fn check_read_only( + mapped: &[Mapped<'_>], + write: &[PolicyPath], + write_name: &str, + roots: &[PathBuf], +) -> Result<()> { + // Write paths narrow the writes of the read-write directory that a child exports whole. + let narrowed = |canonical: &Path| { + roots.iter().any(|root| root == canonical) + && write + .iter() + .any(|path| path.canonical.starts_with(canonical)) + }; + let inside = |inner: &Mapped<'_>, outer_name: &str, outer: &Path, instead: &str| { + Err(Error::unsupported(format!( + "{} entry {} lies inside the {outer_name} entry {}, where the host cannot keep it \ + read-only; instead, {instead}", + inner.name, + inner.given.display(), + outer.display() + ))) + }; + for inner in mapped.iter().filter(|entry| entry.read_only) { + if let Some(path) = write + .iter() + .find(|path| inner.canonical.starts_with(&path.canonical)) + { + return inside( + inner, + write_name, + &path.given, + "narrow the writes to paths outside it", + ); + } + let Some(outer) = mapped.iter().find(|outer| { + !outer.read_only + && !narrowed(&outer.canonical) + && (outer.canonical == inner.canonical + || (outer.directory && inner.canonical.starts_with(&outer.canonical))) + }) else { + continue; + }; + if outer.canonical == inner.canonical { + return Err(Error::unsupported(format!( + "{} entry {} names the object of the {} entry {}, which the guest may write, so \ + the host cannot keep it read-only", + inner.name, + inner.given.display(), + outer.name, + outer.given.display() + ))); + } + return inside( + inner, + outer.name, + &outer.given, + "map that directory read-only and the paths in it that the guest writes read-write", + ); + } + Ok(()) +} + +/// Refuses roots that name one directory or file: OpenVMM refuses one object exported twice, +/// which bind mounts and hard links make possible. +fn distinct_roots(roots: &[PathBuf]) -> Result<()> { let mut identities: Vec<(FileIdentity, &Path)> = Vec::with_capacity(roots.len()); - for root in &roots { + for root in roots { let current = identity(root)?; if let Some((_, other)) = identities.iter().find(|(other, _)| *other == current) { return Err(Error::unsupported(format!( @@ -470,15 +896,47 @@ fn export_roots(mapped: &[Mapped]) -> Result> { } identities.push((current, root)); } - Ok(roots) + Ok(()) } -/// Resolves the denied paths that OpenVMM needs to hide: those that exist, without the ones -/// that other denied paths contain. -fn denied_paths(list: PathList<'_>, mapped: &[Mapped]) -> Result> { - let unsupported = |message: String| Err(Error::unsupported(message)); +/// Resolves the allowed paths of `list`, which must exist inside a child of `roots`, below its +/// root. +fn allowed_paths(list: PathList<'_>, roots: &[PathBuf]) -> Result> { + let field = list.name; + let mut paths: Vec = Vec::with_capacity(list.paths.len()); + for path in list.paths { + let canonical = resolve_policy_path(field, path)?; + if roots.contains(&canonical) { + return Err(Error::unsupported(format!( + "{field} entry {} is an exported path itself rather than a path inside one", + path.display() + ))); + } + if !roots.iter().any(|root| canonical.starts_with(root)) { + return Err(Error::unsupported(format!( + "{field} entry {} lies in no mapped directory", + path.display() + ))); + } + if !paths.iter().any(|other| other.canonical == canonical) { + paths.push(PolicyPath { + given: path.clone(), + canonical, + }); + } + } + Ok(paths) +} + +/// Resolves the denied paths of `list` that exist, and refuses one that hides a mapped path, +/// which `allowed` paths do not show again, or one that does not exist inside a mapped path. +fn denied_paths( + list: PathList<'_>, + mapped: &[Mapped<'_>], + allowed: &[PolicyPath], +) -> Result> { let field = list.name; - let mut denied: Vec = Vec::new(); + let mut resolved = Vec::with_capacity(list.paths.len()); for path in list.paths { let (canonical, exists) = canonicalize_lenient(path).map_err(|error| { Error::unsupported(format!( @@ -487,63 +945,693 @@ fn denied_paths(list: PathList<'_>, mapped: &[Mapped]) -> Result> { )) .with_source(error) })?; - if let Some(entry) = mapped - .iter() - .find(|entry| entry.canonical.starts_with(&canonical)) - { - return unsupported(format!( - "{field} entry {} contains the mapped path {}", - path.display(), - entry.given.display() - )); - } + resolved.push((path, canonical, exists)); + } + let denied_paths: Vec = resolved + .iter() + .filter(|(_, _, exists)| *exists) + .map(|(_, canonical, _)| canonical.clone()) + .collect(); + let allowed_paths = canonicals(allowed); + let mut denied: Vec = Vec::with_capacity(denied_paths.len()); + for (path, canonical, exists) in resolved { if !exists { if let Some(entry) = mapped .iter() .find(|entry| canonical.starts_with(&entry.canonical)) { - return unsupported(format!( + return Err(Error::unsupported(format!( "{field} entry {} does not exist, so it cannot be hidden inside the mapped \ path {}", path.display(), entry.given.display() - )); + ))); } continue; } - denied.push(canonical); + // A mapped directory that is denied itself shows the allowed paths inside it, and nothing + // else, whichever denied path contains it. + let hidden = mapped.iter().find(|entry| { + entry.canonical.starts_with(&canonical) + && match nearest_rule(&entry.canonical, &denied_paths, &allowed_paths, true) { + Some((rule, true)) => { + !(entry.directory + && rule == entry.canonical.as_path() + && allowed_paths.iter().any(|shown| { + *shown != entry.canonical && shown.starts_with(&entry.canonical) + })) + } + _ => false, + } + }); + if let Some(entry) = hidden { + return Err(Error::unsupported(format!( + "{field} entry {} contains the mapped path {}", + path.display(), + entry.given.display() + ))); + } + if !denied.iter().any(|other| other.canonical == canonical) { + denied.push(PolicyPath { + given: path.clone(), + canonical, + }); + } } - Ok(outermost(denied)) + Ok(denied) } -/// Sorts `paths`, drops duplicates, and returns those that no other of them contains. -fn outermost(mut paths: Vec) -> Vec { - paths.sort(); - paths.dedup(); - paths - .iter() - .filter(|path| { - !paths - .iter() - .any(|other| other != *path && path.starts_with(other)) +/// Refuses an allowed path that is also denied, or whose nearest enclosing policy path is not a +/// denied one, which OpenVMM refuses. +fn check_allowed_paths( + allowed_name: &str, + denied_name: &str, + allowed: &[PolicyPath], + denied: &[PolicyPath], +) -> Result<()> { + let (denied_paths, allowed_paths) = (canonicals(denied), canonicals(allowed)); + for path in allowed { + let refused = |problem: String| { + Err(Error::unsupported(format!( + "{allowed_name} entry {} {problem}", + path.given.display() + ))) + }; + if denied_paths.contains(&path.canonical) { + return refused(format!("is also a {denied_name} entry")); + } + match nearest_rule(&path.canonical, &denied_paths, &allowed_paths, false) { + Some((_, true)) => {} + Some((other, false)) => { + return refused(format!( + "lies inside the {allowed_name} entry {}, which shows it already", + other.display() + )); + } + None => { + return refused(format!( + "lies inside no {denied_name} entry, so it has nothing to show again" + )); + } + } + } + Ok(()) +} + +/// Refuses a write path that a denied path hides, which OpenVMM refuses. +fn check_write_paths( + write_name: &str, + denied_name: &str, + write: &[PolicyPath], + denied: &[PolicyPath], + allowed: &[PolicyPath], +) -> Result<()> { + let (denied_paths, allowed_paths) = (canonicals(denied), canonicals(allowed)); + for path in write { + if let Some((rule, true)) = + nearest_rule(&path.canonical, &denied_paths, &allowed_paths, true) + { + return Err(Error::unsupported(format!( + "{write_name} entry {} lies inside the {denied_name} entry {}, which hides it", + path.given.display(), + rule.display() + ))); + } + } + Ok(()) +} + +/// Returns the denied paths of one child that OpenVMM needs to hide, in order: those that no other +/// of them hides already, without an allowed path between them. OpenVMM refuses the others. +/// +/// It takes one child's paths alone: a denied path outside the child, such as one that contains a +/// denied root, hides none of the child for OpenVMM, which receives each child's policy alone. +fn needed_denied_paths(denied: &[PathBuf], allowed: &[PathBuf]) -> Vec { + let mut needed: Vec = denied + .iter() + .filter(|path| { + !nearest_rule(path, denied, allowed, false).is_some_and(|(_, denied)| denied) + }) + .cloned() + .collect(); + needed.sort(); + needed +} + +/// Explains how the policy of `child` breaks a rule of OpenVMM, or returns `None` if it keeps them +/// all: it lists no path twice, hides no path inside another that it hides without an allowed +/// path between them, shows each allowed path again inside a hidden path without another allowed +/// path between them, and makes writable only paths strictly inside it that it shows, none inside +/// another. [`plan`] checks these rules on the paths that it resolves. It reads nothing from the +/// host. +fn policy_rule_problem(child: &ExportChild) -> Option { + let (denied, allowed) = (&child.denied, &child.allowed); + for (paths, kind) in [ + (denied, "hidden"), + (allowed, "allowed"), + (&child.write, "writable"), + ] { + let repeated = paths + .iter() + .enumerate() + .find(|&(position, path)| paths[..position].contains(path)); + if let Some((_, path)) = repeated { + return Some(format!("it lists the {kind} path {} twice", path.display())); + } + } + for path in denied { + if let Some((rule, true)) = nearest_rule(path, denied, allowed, false) { + return Some(format!( + "it hides {} inside {}, which it hides already", + path.display(), + rule.display() + )); + } + } + for path in allowed { + if denied.contains(path) { + return Some(format!("it both hides and shows {}", path.display())); + } + match nearest_rule(path, denied, allowed, false) { + Some((_, true)) => {} + Some((rule, false)) => { + return Some(format!( + "it shows {} again inside {}, which it shows already", + path.display(), + rule.display() + )); + } + None => { + return Some(format!( + "it shows {} again, but hides no path that contains it", + path.display() + )); + } + } + } + for path in &child.write { + if *path == child.root { + return Some(format!( + "it makes all of {} writable through a writable path", + path.display() + )); + } + if let Some(other) = child + .write + .iter() + .find(|other| *other != path && path.starts_with(other)) + { + return Some(format!( + "it makes {} writable inside {}, which it makes writable already", + path.display(), + other.display() + )); + } + if let Some((rule, true)) = nearest_rule(path, denied, allowed, true) { + return Some(format!( + "it makes {} writable, but {} hides it", + path.display(), + rule.display() + )); + } + } + None +} + +/// Returns the nearest of the `denied` and `allowed` paths that contains `path`, which counts +/// itself only when `inclusive`, and whether it is a denied path: the rule by which OpenVMM +/// decides whether the guest sees `path`. +fn nearest_rule<'p>( + path: &Path, + denied: &'p [PathBuf], + allowed: &'p [PathBuf], + inclusive: bool, +) -> Option<(&'p Path, bool)> { + denied + .iter() + .map(|rule| (rule, true)) + .chain(allowed.iter().map(|rule| (rule, false))) + .filter(|(rule, _)| (inclusive || rule.as_path() != path) && path.starts_with(rule)) + .max_by_key(|(rule, _)| rule.components().count()) + .map(|(rule, denied)| (rule.as_path(), denied)) +} + +/// Whether a child is writable, and the paths to which OpenVMM limits its writes. +struct Writable { + /// Whether the guest may modify the child. + writable: bool, + /// The paths to which OpenVMM limits the guest's writes; empty if the guest may modify all of + /// a writable child. + write: Vec, +} + +impl Writable { + /// Returns whether a workload could move `path`, which lies in the child at `root`: whether it + /// lies strictly inside a writable part, in a directory that the workload may change. + fn moves(&self, root: &Path, path: &Path) -> bool { + self.writable + && path != root + && (self.write.is_empty() + || self + .write + .iter() + .any(|write| path != write && path.starts_with(write))) + } +} + +/// Returns the writable part of the child at `root`: its read-write mappings, or, when one of them +/// is the whole child, all of it, unless write paths narrow its writes to them and the read-write +/// mappings inside it. +fn writable_part(root: &Path, mapped: &[Mapped<'_>], write: &[PolicyPath]) -> Writable { + let read_write: Vec = mapped + .iter() + .filter(|entry| !entry.read_only && entry.canonical.starts_with(root)) + .map(|entry| entry.canonical.clone()) + .collect(); + if read_write.is_empty() { + return Writable { + writable: false, + write: Vec::new(), + }; + } + let narrowing: Vec = write + .iter() + .filter(|path| path.canonical.starts_with(root)) + .map(|path| path.canonical.clone()) + .collect(); + let write = if !read_write.iter().any(|path| path == root) { + // Unless a read-write mapping covers the whole child, OpenVMM limits writes to them. + outermost(read_write) + } else if narrowing.is_empty() { + Vec::new() + } else { + outermost( + narrowing + .into_iter() + .chain(read_write.into_iter().filter(|path| path != root)) + .collect(), + ) + }; + Writable { + writable: true, + write, + } +} + +/// Refuses an exported directory or file that belongs to UID 0 or GID 0, to which the caller +/// owner would squash guest root. +fn check_caller_roots(children: &[ExportChild]) -> Result<()> { + #[cfg(unix)] + for child in children { + use std::os::unix::fs::MetadataExt; + + let metadata = fs::metadata(&child.root).map_err(|error| { + Error::unsupported(format!("cannot inspect {}", child.root.display())) + .with_source(error) + })?; + if metadata.uid() == 0 || metadata.gid() == 0 { + return Err(Error::unsupported(format!( + "the caller owner squashes guest root to the owner of each exported directory or \ + file, so {} must not be owned by UID 0 or GID 0", + child.root.display() + ))); + } + } + #[cfg(not(unix))] + let _ = children; + Ok(()) +} + +/// Explains why a mapped host directory, given in canonical form, cannot be exported, or returns +/// `None` if it can: a whole volume. It inspects only the path, not the host. +fn volume_root_refusal(canonical: &Path) -> Option<&'static str> { + (!canonical + .components() + .any(|component| matches!(component, Component::Normal(_)))) + .then_some( + "is a whole volume, which the openvmm backend does not map; map the directories inside \ + it", + ) +} + +/// Explains why `path`, a host path of a recorded plan, is not in the form in which [`plan`] +/// resolves paths, or returns `None` if it is: without a NUL character, which no name holds and +/// no process argument carries; without a `..` component, through which a path that lies inside a +/// directory by its text could leave it; and, on Windows, on a local drive, without the verbatim +/// prefix. It inspects only the path, not the host. +fn resolved_path_problem(path: &Path) -> Option<&'static str> { + if path.as_os_str().as_encoded_bytes().contains(&0) { + return Some("contains a NUL character"); + } + if path + .components() + .any(|component| component == Component::ParentDir) + { + return Some("has a `..` component"); + } + if cfg!(windows) + && !matches!( + path.components().next(), + Some(Component::Prefix(prefix)) if matches!(prefix.kind(), Prefix::Disk(_)) + ) + { + return Some("is not a path on a local drive"); + } + None +} + +/// Sorts `paths`, drops duplicates, and returns those that no other of them contains. +fn outermost(mut paths: Vec) -> Vec { + paths.sort(); + paths.dedup(); + paths + .iter() + .filter(|path| { + !paths + .iter() + .any(|other| other != *path && path.starts_with(other)) }) .cloned() .collect() } impl ExportPlan { + /// Checks that a plan read back from a record is one that [`plan`] could have made for a root + /// that reserves `reserved`: at least one and at most [`MAX_CHILDREN`] children, none inside + /// another, each an absolute host path below a volume's root, with its host paths in the form + /// in which `plan` resolves them, whose policy paths lie inside it within OpenVMM's limits and + /// keep its rules, with an identity slot for each denied path, and writable exactly when a bind + /// maps a path of it read-write; and at least one and at most [`MAX_HOST_MAPPINGS`] binds, + /// each of a known child, from a relative source that holds the host's names joined with `/`, + /// that a guest path can hold, and that the child does not hide, unless it shows paths inside + /// it again, to a free guest target, parents before children, with a bind of every child's + /// root. A target inside another's must show what the other shows there. A child's write + /// paths must be the outermost paths of its read-write binds, unless one maps the whole child, + /// when they may narrow its writes only to paths that keep its other read-write binds + /// writable; and each denied or mapped object that a workload could move, strictly inside a + /// part of the child that the guest may write, must carry its identity. It reads nothing from + /// the host; [`verify`](Self::verify) checks the host. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). + pub fn check(&self, reserved: ReservedTargets<'_>) -> Result<()> { + let invalid = |reason: String| { + Err(Error::unsupported(format!( + "the recorded export plan is invalid: {reason}" + ))) + }; + if self.children.is_empty() || self.children.len() > MAX_CHILDREN { + return invalid(format!( + "it exports {} directories and files, but OpenVMM exports from 1 to \ + {MAX_CHILDREN}", + self.children.len() + )); + } + if self.binds.is_empty() || self.binds.len() > MAX_HOST_MAPPINGS { + return invalid(format!( + "it maps {} paths, but a sandbox maps from 1 to {MAX_HOST_MAPPINGS}", + self.binds.len() + )); + } + let mut policy_bytes = 0; + for (index, child) in self.children.iter().enumerate() { + if !child.root.is_absolute() || volume_root_refusal(&child.root).is_some() { + return invalid(format!( + "child {index}, {}, is not an absolute path below a volume's root", + child.root.display() + )); + } + if let Some((path, problem)) = std::iter::once(&child.root) + .chain(&child.denied) + .chain(&child.allowed) + .chain(&child.write) + .find_map(|path| resolved_path_problem(path).map(|problem| (path, problem))) + { + return invalid(format!( + "child {index} has the host path {path:?}, which {problem}" + )); + } + if child.denied_identities.len() != child.denied.len() { + return invalid(format!( + "child {index} has {} denied paths but {} identities", + child.denied.len(), + child.denied_identities.len() + )); + } + if let Some(path) = [&child.denied, &child.allowed, &child.write] + .into_iter() + .flatten() + .find(|path| !path.starts_with(&child.root) || (child.file && *path != &child.root)) + { + return invalid(format!( + "child {index} has the policy path {}, which is not inside it", + path.display() + )); + } + for (paths, action) in [ + (&child.denied, "hide"), + (&child.allowed, "show again"), + (&child.write, "make writable"), + ] { + match policy_limits(&child.root, paths, action) { + Ok(bytes) => policy_bytes += bytes, + Err(problem) => return invalid(format!("child {index}: {problem}")), + } + } + if let Some(other) = self.children[..index].iter().position(|other| { + other.root.starts_with(&child.root) || child.root.starts_with(&other.root) + }) { + return invalid(format!( + "child {index}, {}, overlaps child {other}, {}", + child.root.display(), + self.children[other].root.display() + )); + } + if let Some(problem) = policy_rule_problem(child) { + return invalid(format!("child {index}: {problem}")); + } + if !child.writable && !child.write.is_empty() { + return invalid(format!("child {index} is read-only but has writable paths")); + } + if !self + .binds + .iter() + .any(|bind| bind.child == index && bind.source.is_empty()) + { + return invalid(format!("it maps no path at the root of child {index}")); + } + // OpenVMM lets the guest write a child that a read-write bind needs, and no other. + let read_write = self + .binds + .iter() + .any(|bind| bind.child == index && !bind.read_only); + if child.writable != read_write { + return invalid(if child.writable { + format!("child {index} is writable but maps no path read-write") + } else { + format!("child {index} is read-only but maps a path read-write") + }); + } + } + if policy_bytes > MAX_AGGREGATE_POLICY_BYTES { + return invalid(format!( + "the paths that OpenVMM is to hide, show again, or make writable exceed its \ + {MAX_AGGREGATE_POLICY_BYTES}-byte limit" + )); + } + let mut previous: Option<&Bind> = None; + for (index, bind) in self.binds.iter().enumerate() { + let Some(child) = self.children.get(bind.child) else { + return invalid(format!( + "it maps a path of the unknown child {}", + bind.child + )); + }; + if !bind.source.is_empty() + && (child.file + || bind + .source + .split('/') + .any(|component| matches!(component, "" | "." | ".."))) + { + return invalid(format!( + "it maps the path {:?} of child {}, which is not a path inside a directory", + bind.source, bind.child + )); + } + // The guest receives the source as a path below its mount of the export. + if bind.source.contains('\0') { + return invalid(format!( + "it maps the path {:?} of child {}, which contains a NUL character", + bind.source, bind.child + )); + } + if GUEST_EXPORT.len() + 1 + bind.guest_source().len() > MAX_GUEST_PATH { + return invalid(format!( + "it maps a path of {} bytes in child {}, which is too long for a guest path", + bind.source.len(), + bind.child + )); + } + let path = if bind.source.is_empty() { + child.root.clone() + } else { + child.root.join(&bind.source) + }; + // The host checks the object at that path, which must be the one that the guest maps: + // the source must hold the host's names below the root, joined with `/`, as `plan` + // spells them. A Windows host would read a backslash in a name as a separator, and a + // name with a root or a drive as another path. + if relative_text(&child.root, &path).as_deref() != Some(bind.source.as_str()) { + return invalid(format!( + "it maps the path {:?} of child {}, which the host reads as other names", + bind.source, bind.child + )); + } + // A mapped path shows something: OpenVMM does not hide it, or it shows the allowed + // paths inside it again. + if matches!( + nearest_rule(&path, &child.denied, &child.allowed, true), + Some((_, true)) + ) && !child + .allowed + .iter() + .any(|shown| *shown != path && shown.starts_with(&path)) + { + return invalid(format!( + "it maps {} at {}, but child {} hides it", + path.display(), + bind.target, + bind.child + )); + } + if let Some(problem) = target_problem(&bind.target) { + return invalid(format!("its guest target {:?} {problem}", bind.target)); + } + if let Some(reserved) = reserved.reservation(&bind.target) { + return invalid(format!( + "its guest target {} would cover the guest's {reserved}", + bind.target + )); + } + if let Some(previous) = previous + && (depth(&previous.target), previous.target.as_str()) + >= (depth(&bind.target), bind.target.as_str()) + { + return invalid(format!( + "it maps {} after {}, out of order or twice", + bind.target, previous.target + )); + } + // A target inside another shows what the other shows there, as `plan` requires. + for outer in &self.binds[..index] { + let Some(rest) = bind + .target + .strip_prefix(outer.target.as_str()) + .and_then(|rest| rest.strip_prefix('/')) + else { + continue; + }; + let shown = if outer.source.is_empty() { + rest.to_owned() + } else { + format!("{}/{rest}", outer.source) + }; + if bind.child != outer.child || bind.source != shown { + return invalid(format!( + "it maps {} inside {}, where it would hide part of that path", + bind.target, outer.target + )); + } + } + previous = Some(bind); + } + for (index, child) in self.children.iter().enumerate() { + let bound = |bind: &Bind| { + if bind.source.is_empty() { + child.root.clone() + } else { + child.root.join(&bind.source) + } + }; + let read_write: Vec = self + .binds + .iter() + .filter(|bind| bind.child == index && !bind.read_only) + .map(bound) + .collect(); + // A child that a read-write bind maps whole is writable throughout, unless write paths + // narrow its writes, which must then keep its other read-write binds writable; any + // other child's writes are narrowed to exactly what its read-write binds map. + if read_write.contains(&child.root) { + if let Some(path) = read_write.iter().find(|path| { + **path != child.root + && !child.write.is_empty() + && !child.write.iter().any(|write| path.starts_with(write)) + }) { + return invalid(format!( + "child {index} narrows its writes so that {}, which it maps read-write, \ + stays read-only", + path.display() + )); + } + } else { + let mut write = child.write.clone(); + write.sort(); + if outermost(read_write) != write { + return invalid(format!( + "child {index} lets the guest write other paths than those that it maps \ + read-write" + )); + } + } + // A workload that may write a part of the child could move an object there and put a + // decoy at its path, so the plan pins each denied and mapped object in such a part, as + // `plan` does, for `verify` to check it at the next start. + let part = Writable { + writable: child.writable, + write: child.write.clone(), + }; + if let Some((path, _)) = child + .denied + .iter() + .zip(&child.denied_identities) + .find(|(path, identity)| identity.is_none() && part.moves(&child.root, path)) + { + return invalid(format!( + "child {index} does not pin the denied path {}, which a workload could move", + path.display() + )); + } + if let Some(bind) = self.binds.iter().find(|bind| { + bind.child == index + && bind.identity.is_none() + && part.moves(&child.root, &bound(bind)) + }) { + return invalid(format!( + "it does not pin the object that it maps at {}, which a workload could move", + bind.target + )); + } + } + Ok(()) + } + /// Checks that the host keeps every read-only mapping read-only, that every child's root is - /// still the kind of object that [`plan`] exported, and that every mapped and denied path - /// inside a read-write mapping still names the object it named then. + /// still the kind of object that [`plan`] exported, that every mapped and denied path inside a + /// writable part still names the object it named then, and that the host still supports the + /// owner. /// - /// [`plan`] refuses a read-only path inside a read-write directory, but a plan that an earlier - /// crate recorded may hold one, which only the guest's bind mount kept read-only; it fails - /// with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). A file that a directory - /// replaced would otherwise grant the whole directory, and a workload with a read-write - /// mapping could rename a denied or mapped object inside it and put a decoy at its path, so - /// the next start would hide or bind the decoy instead; these fail with - /// [`ErrorKind::Other`](crate::ErrorKind::Other). Run it while the VM is stopped, so that no - /// workload can race with it; OpenVMM checks each child's kind again when it opens it. + /// [`plan`] refuses a read-only path inside a part that the guest may write, which only the + /// guest's bind mount would keep read-only; a recorded plan that holds one fails with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). A file that a directory replaced + /// would otherwise grant the whole directory, and a workload that may write a directory could + /// rename a denied or mapped object inside it and put a decoy at its path, so the next start + /// would hide or bind the decoy instead; these fail with + /// [`ErrorKind::Other`](crate::ErrorKind::Other). With the caller owner, the host must be a + /// Linux host, and no exported directory or file may belong to UID 0 or GID 0; these fail with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). Run it while the VM is stopped, + /// so that no workload can race with it; OpenVMM checks each child's kind again when it opens + /// it. pub fn verify(&self) -> Result<()> { for bind in self.binds.iter().filter(|bind| bind.read_only) { let (child, path) = self.bound(bind)?; @@ -601,6 +1689,12 @@ impl ExportPlan { } } } + if self.owner == Owner::Caller { + if cfg!(windows) { + return Err(Error::unsupported(CALLER_OWNER_ON_WINDOWS)); + } + check_caller_roots(&self.children)?; + } Ok(()) } @@ -637,9 +1731,9 @@ impl ExportPlan { .collect() } - /// OpenVMM options that export the plan's directories and files with their access policies. - /// A file child carries the `file` flag, so OpenVMM refuses a directory that replaced the - /// file. + /// OpenVMM options that export the plan's directories and files at [`GUEST_EXPORT`], with + /// their access policies and the owner. A file child carries the `file` flag, so OpenVMM + /// refuses a directory that replaced the file. pub fn openvmm_arguments(&self) -> Vec { let mut arguments = vec!["--mount-aggregate".to_owned(), GUEST_EXPORT.to_owned()]; for (index, child) in self.children.iter().enumerate() { @@ -651,32 +1745,43 @@ impl ExportPlan { display_path(&child.root) )); } - let mut add = |option: &str, path: &Path| { - arguments.push(option.to_owned()); - arguments.push(display_path(path)); - }; - for child in self.children.iter().filter(|child| child.hidden) { - add("--mount-deny", &child.root); - } - for child in &self.children { - for path in &child.denied { - add("--mount-deny", path); - } - } - for child in &self.children { - for path in &child.allowed { - add("--mount-allow", path); + for (option, paths) in [ + ( + "--mount-deny", + ExportChild::denied_paths as fn(&ExportChild) -> &[PathBuf], + ), + ("--mount-allow", ExportChild::allowed_paths), + ("--mount-write", ExportChild::write_paths), + ] { + for path in self.children.iter().flat_map(paths) { + arguments.push(option.to_owned()); + arguments.push(display_path(path)); } } - for child in &self.children { - for path in &child.write { - add("--mount-write", path); - } + if self.owner == Owner::Caller { + arguments.extend(["--mount-owner".to_owned(), "caller".to_owned()]); } arguments } } +impl ExportChild { + /// Returns the paths that OpenVMM hides. + fn denied_paths(&self) -> &[PathBuf] { + &self.denied + } + + /// Returns the paths that stay visible inside denied paths. + fn allowed_paths(&self) -> &[PathBuf] { + &self.allowed + } + + /// Returns the paths to which OpenVMM limits the guest's writes. + fn write_paths(&self) -> &[PathBuf] { + &self.write + } +} + /// Applies OpenVMM's rules for exported directories, which it reaches without links. fn check_root(root: &Path) -> Result<()> { let mut current = PathBuf::new(); @@ -699,44 +1804,22 @@ fn check_root(root: &Path) -> Result<()> { Ok(()) } -/// Applies OpenVMM's rules for the paths that it is to `action` in the exported directory -/// `root`, so a policy that OpenVMM would refuse fails at provision rather than at every start. -/// Returns their combined length. +/// Applies OpenVMM's rules for the paths that it is to `action` in the exported directory `root`, +/// so a policy that OpenVMM would refuse fails at provision rather than at every start: those of +/// [`policy_limits`], and on the host, no link or reparse point below `root` and no other file +/// system. Returns their combined length. fn check_policy_paths(root: &Path, paths: &[PathBuf], action: &str) -> Result { let unsupported = |message: String| Err(Error::unsupported(message)); - let count = paths.len(); - if count > MAX_POLICY_PATHS { - return unsupported(format!( - "OpenVMM can {action} at most {MAX_POLICY_PATHS} paths in one exported directory, \ - but {} needs {count}", - root.display() - )); - } - let mut total = 0; + let total = policy_limits(root, paths, action).map_err(Error::unsupported)?; for path in paths { - let Some(relative) = relative_text(root, path) else { - return unsupported(format!("{} is not valid UTF-8", path.display())); - }; - // A name may contain spaces, but not begin or end with one. - if relative.len() > MAX_POLICY_PATH_BYTES - || relative.chars().any(|character| { - (character.is_whitespace() && character != ' ') || matches!(character, ':' | '\\') - }) - || relative - .split('/') - .any(|name| name.starts_with(' ') || name.ends_with(' ')) - { - return unsupported(format!( - "OpenVMM cannot {action} {}: below the exported directory {}, a path must not \ - contain colons, backslashes, or whitespace other than spaces, and no name may \ - begin or end with a space", - path.display(), - root.display() - )); - } - total += relative.len(); + // `policy_limits` read each path as text below `root` already. + let relative = relative_text(root, path).unwrap_or_default(); + // The exported directory itself, which a denied root names, was checked as a root. + let components = relative + .split('/') + .filter(|component| !component.is_empty()); let mut current = root.to_path_buf(); - for component in relative.split('/') { + for component in components { current.push(component); let metadata = fs::symlink_metadata(¤t).map_err(|error| { Error::unsupported(format!("cannot inspect {}", current.display())) @@ -754,15 +1837,61 @@ fn check_policy_paths(root: &Path, paths: &[PathBuf], action: &str) -> Result std::result::Result { + let count = paths.len(); + if count > MAX_POLICY_PATHS { + return Err(format!( + "OpenVMM can {action} at most {MAX_POLICY_PATHS} paths in one exported directory, \ + but {} needs {count}", + root.display() + )); + } + let mut total = 0; + for path in paths { + let Some(relative) = relative_text(root, path) else { + return Err(format!("{} is not valid UTF-8", path.display())); + }; + // A name may contain spaces, but not begin or end with one. + if relative.len() > MAX_POLICY_PATH_BYTES + || relative.chars().any(|character| { + (character.is_whitespace() && character != ' ') || matches!(character, ':' | '\\') + }) + || relative + .split('/') + .any(|name| name.starts_with(' ') || name.ends_with(' ')) + { + return Err(format!( + "OpenVMM cannot {action} {}: below the exported directory {}, a path must not \ + contain colons, backslashes, or whitespace other than spaces, and no name may \ + begin or end with a space", path.display(), root.display() )); } + total += relative.len(); } if total > MAX_POLICY_BYTES { - return unsupported(format!( + return Err(format!( "the paths that OpenVMM is to {action} in {} exceed its {MAX_POLICY_BYTES}-byte \ limit", root.display() @@ -804,7 +1933,9 @@ fn relative_text(root: &Path, path: &Path) -> Option { } /// Canonicalized, validated form of one requested host mapping. -struct Mapped { +struct Mapped<'a> { + /// How messages name the list or option that requested it. + name: &'a str, /// Host path exactly as the caller supplied it, for diagnostics. given: PathBuf, /// Link-resolved host path used to construct the export. @@ -818,12 +1949,13 @@ struct Mapped { } /// Returns whether the guest path `path` is `directory` or one of its descendants, comparing -/// whole components: `/run/nvx` is within `/run`, but `/runner` is not. +/// whole components: `/run/nvx` is within `/run`, but `/runner` is not, and every absolute path is +/// within the root, `/`. pub(crate) fn within(path: &str, directory: &str) -> bool { path == directory || path .strip_prefix(directory) - .is_some_and(|rest| rest.starts_with('/')) + .is_some_and(|rest| directory.ends_with('/') || rest.starts_with('/')) } /// Counts the nonempty components of an absolute guest path. @@ -831,16 +1963,28 @@ fn depth(path: &str) -> usize { path.split('/').filter(|part| !part.is_empty()).count() } -/// Resolves links and returns a path without the Windows verbatim prefix. +/// Resolves links and returns a path without the Windows verbatim prefix, which must still name +/// the object that the resolved path names. +/// +/// Windows reads a path without the prefix differently: it drops a trailing period or space of a +/// name, among other things. A path that resolves to itself again names the same object; one that +/// only the verbatim prefix keeps, such as that of a directory whose name ends in a period, would +/// name another object, or none, so it is refused. fn canonicalize(path: &Path) -> std::io::Result { let canonical = fs::canonicalize(path)?; - match strip_verbatim(&canonical) { - Some(stripped) => Ok(stripped), - None => Err(std::io::Error::new( + let Some(stripped) = strip_verbatim(&canonical) else { + return Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "network paths are unsupported", - )), + )); + }; + if stripped != canonical && !fs::canonicalize(&stripped).is_ok_and(|again| again == canonical) { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "only a verbatim path names it, as with a name that ends in a period or a space", + )); } + Ok(stripped) } /// Canonicalizes the deepest existing ancestor and appends the rest. Returns whether the @@ -910,19 +2054,27 @@ mod tests { } fn plan(policy: &FilesystemPolicy) -> Result> { - super::plan(&ExportRequest { - read_only: PathList { + let mappings: Vec> = Mapping::convention( + PathList { name: "filesystem.readonlyPaths", paths: &policy.readonly_paths, }, - read_write: PathList { + true, + ) + .chain(Mapping::convention( + PathList { name: "filesystem.readwritePaths", paths: &policy.readwrite_paths, }, + false, + )) + .collect(); + super::plan(&ExportRequest { denied: PathList { name: "filesystem.deniedPaths", paths: &policy.denied_paths, }, + ..ExportRequest::new(&mappings, ReservedTargets::BLOCKLESS_ROOT) }) } @@ -996,6 +2148,9 @@ mod tests { ("/run", "/run"), ("/run/nvx", "/run"), ("/run/nvx/t", "/run"), + ("/", "/"), + ("/run", "/"), + ("/run/nvx", "/"), ] { assert!(within(path, directory), "{path} {directory}"); } @@ -1003,6 +2158,7 @@ mod tests { ("/runner", "/run"), ("/run", "/run/nvx"), ("/srv/run", "/run"), + ("/", "/run"), ] { assert!(!within(path, directory), "{path} {directory}"); } @@ -1020,17 +2176,17 @@ mod tests { ..FilesystemPolicy::default() }; let mapping = plan(&policy).unwrap().unwrap(); - let children: Vec<(PathBuf, bool, bool)> = mapping + let children: Vec<(PathBuf, bool)> = mapping .children .iter() - .map(|child| (child.root.clone(), child.writable, child.hidden)) + .map(|child| (child.root.clone(), child.writable)) .collect(); assert_eq!( children, [ - (canonical(&base.join("build-output")), true, false), - (canonical(&base.join("projects/app")), true, false), - (canonical(&base.join("tools")), false, false), + (canonical(&base.join("build-output")), true), + (canonical(&base.join("projects/app")), true), + (canonical(&base.join("tools")), false), ] ); assert!(mapping.children.iter().all(|child| child.write.is_empty() @@ -1160,18 +2316,18 @@ mod tests { let mapping = plan(&policy).unwrap().unwrap(); let work = canonical(&base.join("work")); // Each file is a child of its own, without its directory, beside the directories. - let children: Vec<(PathBuf, bool, bool, bool)> = mapping + let children: Vec<(PathBuf, bool, bool)> = mapping .children .iter() - .map(|child| (child.root.clone(), child.file, child.writable, child.hidden)) + .map(|child| (child.root.clone(), child.file, child.writable)) .collect(); assert_eq!( children, [ - (work.join("config.json"), true, false, false), - (work.join("out"), false, true, false), - (work.join("run.sh"), true, true, false), - (work.join("src"), false, false, false), + (work.join("config.json"), true, false), + (work.join("out"), false, true), + (work.join("run.sh"), true, true), + (work.join("src"), false, false), ] ); assert!( @@ -1269,7 +2425,7 @@ mod tests { mapping .children .iter() - .all(|child| child.file && !child.hidden && child.allowed.is_empty()) + .all(|child| child.file && child.allowed.is_empty()) ); // More are refused rather than exported through their directory, which would grant @@ -1316,7 +2472,7 @@ mod tests { assert_eq!(mapping.children.len(), 1); let child = &mapping.children[0]; let work = canonical(&base.join("work")); - assert!(!child.hidden && child.writable); + assert!(child.writable); // The read-only root is read-only on the host, except the outermost read-write mapping // inside it, to which OpenVMM limits writes. assert_eq!(child.write, [work.join("out")]); @@ -1603,6 +2759,50 @@ mod tests { assert_eq!(resolve_guest_path(&upper), Some(expected)); } + #[cfg(windows)] + #[test] + fn paths_that_only_a_verbatim_path_names_are_refused() { + let directory = root(); + let base = directory.path(); + // A verbatim path keeps a trailing period or space of a name, which Windows drops from a + // path without the prefix: there, `work.` and `work ` would name `work`, and `solo.` + // nothing. + let verbatim = fs::canonicalize(base).unwrap(); + let named = |name: &str| PathBuf::from(format!(r"{}\{name}", verbatim.display())); + for name in ["work.", "work ", "solo.", r"work\src\secret."] { + fs::create_dir(named(name)).unwrap(); + } + for name in ["work.", "work ", "solo."] { + let path = named(name); + let error = canonicalize(&path).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::Unsupported, "{error}"); + let policy = FilesystemPolicy { + readwrite_paths: vec![path], + ..FilesystemPolicy::default() + }; + let error = plan(&policy).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!(error.to_string().contains("is not accessible"), "{error}"); + } + // A denied path that only a verbatim path names would hide another path instead. + let policy = FilesystemPolicy { + readwrite_paths: vec![base.join("work")], + denied_paths: vec![named(r"work\src\secret.")], + ..FilesystemPolicy::default() + }; + let error = plan(&policy).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!(error.to_string().contains("cannot be resolved"), "{error}"); + // A verbatim path that names what the path without its prefix names is planned as that + // path. + let policy = FilesystemPolicy { + readwrite_paths: vec![named("work")], + ..FilesystemPolicy::default() + }; + let exported = plan(&policy).unwrap().unwrap(); + assert_eq!(exported.children[0].root, canonical(&base.join("work"))); + } + #[test] fn policy_paths_follow_openvmm_rules() { let directory = root(); @@ -1742,9 +2942,8 @@ mod tests { assert_eq!(mapping.verify().unwrap_err().kind(), ErrorKind::Other); } - /// A plan that an earlier crate recorded for a read-only path inside a read-write directory - /// holds a read-only bind in a part of a child that the guest may write, which only the - /// guest's bind mount kept read-only, so verification refuses it before a guest runs. + /// A recorded plan with a read-only bind in a part of a child that the guest may write, which + /// only the guest's bind mount would keep read-only, is refused before a guest runs. #[test] fn read_only_binds_that_the_host_cannot_enforce_fail_verification() { let directory = root(); @@ -1823,4 +3022,705 @@ mod tests { .collect(); assert!(modes.contains(&true) && modes.contains(&false)); } + /// Returns a mapping of `path` at the guest path `target`. + fn at<'a>(path: &'a Path, target: &'a str, read_only: bool) -> Mapping<'a> { + Mapping { + name: "mappings", + path, + target: Target::Guest(target), + read_only, + } + } + + #[test] + fn explicit_targets_place_mappings_at_any_free_guest_path() { + let directory = root(); + let base = directory.path(); + let (work, tools) = (base.join("work"), base.join("tools")); + let config = work.join("config.json"); + let mappings = [ + at(&work, "/workspace", false), + at(&tools, "/opt/tools", true), + // A target inside another shows what the other shows there. + at(&config, "/workspace/config.json", false), + // One object may appear at several targets. + at(&work, "/mirror", false), + ]; + let request = ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT); + let mapping = super::plan(&request).unwrap().unwrap(); + let roots: Vec<&Path> = mapping + .children + .iter() + .map(|child| child.root.as_path()) + .collect(); + assert_eq!(roots, [canonical(&tools), canonical(&work)]); + let binds: Vec<(&str, String, bool)> = mapping + .binds + .iter() + .map(|bind| (bind.target.as_str(), bind.guest_source(), bind.read_only)) + .collect(); + assert_eq!( + binds, + [ + ("/mirror", "1".to_owned(), false), + ("/workspace", "1".to_owned(), false), + ("/opt/tools", "0".to_owned(), true), + ("/workspace/config.json", "1/config.json".to_owned(), false), + ] + ); + mapping.check(ReservedTargets::CONTAINER_ROOT).unwrap(); + // A root that keeps its whole tree refuses every mapping, planned or recorded. + let everything = ReservedTargets { + trees: &["/"], + exact: &[], + }; + for error in [ + super::plan(&ExportRequest::new(&mappings, everything)).unwrap_err(), + mapping.check(everything).unwrap_err(), + ] { + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.message().ends_with("would cover the guest's /"), + "{error}" + ); + } + // The convention's plans pass the check of their root too. + let convention = plan(&FilesystemPolicy { + readonly_paths: vec![tools.clone()], + readwrite_paths: vec![work.clone(), config.clone()], + ..FilesystemPolicy::default() + }) + .unwrap() + .unwrap(); + convention.check(ReservedTargets::BLOCKLESS_ROOT).unwrap(); + + let refused = |mappings: &[Mapping<'_>], expected: &str| { + let request = ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT); + let error = super::plan(&request).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.message().contains(expected), + "{error} lacks {expected}" + ); + }; + refused( + &[at(&work, "/data", true), at(&tools, "/data", true)], + "have the same guest target /data", + ); + refused( + &[at(&work, "/data", true), at(&tools, "/data/tools", true)], + "at /data/tools would hide part of the mappings entry", + ); + // A file shows nothing below it. + refused( + &[at(&config, "/data", true), at(&tools, "/data/tools", true)], + "would hide part of", + ); + let long = format!("/{}", "x".repeat(4095)); + for (target, problem) in [ + ("data", "is not absolute"), + ("/", "is the guest's root"), + ("/a//b", "has an empty, `.`, or `..` component"), + ("/a/./b", "has an empty, `.`, or `..` component"), + ("/a/../b", "has an empty, `.`, or `..` component"), + ("/a/", "has an empty, `.`, or `..` component"), + ("/a\0b", "contains a NUL character"), + (long.as_str(), "is longer than a guest path may be"), + ] { + refused(&[at(&work, target, true)], problem); + } + refused( + &[at(&work, "/etc", true)], + "has the guest target /etc, which would cover the guest's /etc", + ); + refused( + &[at(&work, "/proc/self", true)], + "would cover the guest's /proc", + ); + // The same object read-only and read-write at two targets would let the guest write it. + refused( + &[at(&work, "/a", false), at(&work, "/b", true)], + "names the object of the mappings entry", + ); + // A target inside another must spell, name for name, the host path below the other's + // object, so a backslash in a guest name, which a Windows host reads as a separator, and a + // guest name that a Windows host reads as a root of its own show nothing there. + let host = work.join("src").join("host"); + fs::create_dir_all(&host).unwrap(); + refused( + &[ + at(&work, "/workspace", true), + at(&host, r"/workspace/src\host", true), + ], + "would hide part of", + ); + let resolved = canonical(&work); + let text = resolved.to_str().unwrap(); + let rooted = text + .split_once(':') + .map_or(text, |(_, rest)| rest) + .replace('/', r"\"); + let inside = format!("/opt/tools/{rooted}"); + refused( + &[at(&tools, "/opt/tools", true), at(&work, &inside, true)], + "would hide part of", + ); + } + + #[test] + fn reserved_targets_depend_on_the_root() { + let container = ReservedTargets::CONTAINER_ROOT; + for (target, reserved) in [ + ("/", "/"), + ("/proc", "/proc"), + ("/proc/self", "/proc"), + ("/sys/fs", "/sys"), + ("/dev/shm", "/dev"), + ("/.nvx-agent/tools", "/.nvx-agent"), + ("/etc", "/etc"), + ("/etc/machine-id", "/etc/machine-id"), + ("/run", "/run"), + // The ancestors of the runtime's trees are reserved, as exact paths here. + ("/run/nvx", "/run/nvx"), + ("/run/nvx/time/state", "/run/nvx/time"), + ] { + assert_eq!(container.reservation(target), Some(reserved), "{target}"); + } + for target in [ + "/etc/app", + "/run/secrets", + "/run/nvx/other", + "/usr/local", + "/workspace", + "/procfs", + ] { + assert_eq!(container.reservation(target), None, "{target}"); + } + let blockless = ReservedTargets::BLOCKLESS_ROOT; + for (target, reserved) in [ + ("/", "/"), + ("/usr/local", "/usr"), + ("/run", "/run"), + ("/etc/app", "/etc"), + ("/init", "/init"), + ] { + assert_eq!(blockless.reservation(target), Some(reserved), "{target}"); + } + for target in ["/workspace", "/mnt/c/work", "/home/me", "/initrd", "/opt"] { + assert_eq!(blockless.reservation(target), None, "{target}"); + } + // A mapping at an ancestor of a reserved tree would shadow it. + let nested = ReservedTargets { + trees: &["/srv/runtime/state"], + exact: &[], + }; + assert_eq!(nested.reservation("/srv"), Some("/srv/runtime/state")); + assert_eq!( + nested.reservation("/srv/runtime"), + Some("/srv/runtime/state") + ); + assert_eq!(nested.reservation("/srv/other"), None); + // A root that keeps its whole tree takes no mapping. + let everything = ReservedTargets { + trees: &["/"], + exact: &[], + }; + for target in ["/", "/workspace", "/srv/runtime/state"] { + assert_eq!(everything.reservation(target), Some("/"), "{target}"); + } + } + + #[test] + fn policy_paths_hide_show_and_narrow_writes() { + let directory = root(); + let base = directory.path(); + fs::create_dir_all(base.join("work/src/secret/public")).unwrap(); + fs::create_dir_all(base.join("work/out/cache")).unwrap(); + let work = base.join("work"); + let paths = + |names: &[&str]| -> Vec { names.iter().map(|name| work.join(name)).collect() }; + let request = |mappings: &[Mapping<'_>], + denied: &[PathBuf], + allowed: &[PathBuf], + write: &[PathBuf]| { + let request = ExportRequest { + denied: PathList { + name: "denied", + paths: denied, + }, + allowed: PathList { + name: "allowed", + paths: allowed, + }, + write: PathList { + name: "write", + paths: write, + }, + ..ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT) + }; + super::plan(&request) + }; + let whole = [at(&work, "/workspace", false)]; + let (denied, allowed, write) = ( + paths(&["src/secret"]), + paths(&["src/secret/public"]), + paths(&["out"]), + ); + let mapping = request(&whole, &denied, &allowed, &write).unwrap().unwrap(); + let child = &mapping.children[0]; + let resolved = canonical(&work); + assert!(child.writable); + assert_eq!(child.denied, [resolved.join("src").join("secret")]); + assert_eq!( + child.allowed, + [resolved.join("src").join("secret").join("public")] + ); + assert_eq!(child.write, [resolved.join("out")]); + // Writes are narrowed to `out`, so nothing outside it can move. + assert_eq!(child.denied_identities, [None]); + let arguments = mapping.openvmm_arguments(); + let text = |path: PathBuf| vec![display_path(&path)]; + assert_eq!( + option_values(&arguments, "--mount-deny"), + text(resolved.join("src").join("secret")) + ); + assert_eq!( + option_values(&arguments, "--mount-allow"), + text(resolved.join("src").join("secret").join("public")) + ); + assert_eq!( + option_values(&arguments, "--mount-write"), + text(resolved.join("out")) + ); + assert!(!arguments.contains(&"--mount-owner".to_owned())); + mapping.check(ReservedTargets::CONTAINER_ROOT).unwrap(); + + // A denied root shows only its allowed paths. + let mapping = request(&whole, std::slice::from_ref(&work), &paths(&["src"]), &[]) + .unwrap() + .unwrap(); + assert_eq!(mapping.children[0].denied, std::slice::from_ref(&resolved)); + assert_eq!(mapping.children[0].allowed, [resolved.join("src")]); + let error = request(&whole, std::slice::from_ref(&work), &[], &[]).unwrap_err(); + assert!( + error.message().contains("contains the mapped path"), + "{error}" + ); + + // A read-only path may lie in the part of a narrowed directory that stays read-only, and + // the directory may be mapped read-only elsewhere too. + let src = work.join("src"); + let parts = [ + at(&work, "/workspace", false), + at(&src, "/src", true), + at(&work, "/view", true), + ]; + request(&parts, &[], &[], &write).unwrap().unwrap(); + + let refused = |mappings: &[Mapping<'_>], + denied: &[PathBuf], + allowed: &[PathBuf], + write: &[PathBuf], + expected: &str| { + let error = request(mappings, denied, allowed, write).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.message().contains(expected), + "{error} lacks {expected}" + ); + }; + refused( + &whole, + &[], + &paths(&["src"]), + &[], + "lies inside no denied entry", + ); + refused( + &whole, + std::slice::from_ref(&work), + std::slice::from_ref(&work), + &[], + "is an exported path itself", + ); + refused( + &whole, + &paths(&["src"]), + &paths(&["src/secret", "src/secret/public"]), + &[], + "which shows it already", + ); + refused( + &whole, + &paths(&["src"]), + &paths(&["src"]), + &[], + "is also a denied entry", + ); + refused( + &[at(&work, "/workspace", true)], + &[], + &[], + &write, + "lies in no directory that is mapped read-write as a whole", + ); + refused( + &whole, + &[], + &[], + std::slice::from_ref(&work), + "lies in no directory that is mapped read-write as a whole", + ); + refused( + &whole, + &paths(&["src"]), + &[], + &paths(&["src/secret"]), + "which hides it", + ); + let cache = work.join("out/cache"); + refused( + &[at(&work, "/workspace", false), at(&cache, "/cache", true)], + &[], + &[], + &write, + "lies inside the write entry", + ); + refused( + &[at(&work, "/workspace", false), at(&work, "/view", true)], + &[], + &[], + &[], + "names the object of the mappings entry", + ); + } + + #[test] + fn narrowed_writes_pin_only_what_lies_in_writable_paths() { + let directory = root(); + let base = directory.path(); + fs::create_dir_all(base.join("work/out/cache")).unwrap(); + fs::create_dir_all(base.join("work/out/private")).unwrap(); + let work = base.join("work"); + let (cache, src) = (work.join("out/cache"), work.join("src")); + let mappings = [ + at(&work, "/workspace", false), + at(&cache, "/cache", false), + at(&src, "/src", true), + ]; + let denied = [work.join("out/private"), work.join("src/secret")]; + let write = [work.join("out")]; + let request = ExportRequest { + denied: PathList { + name: "denied", + paths: &denied, + }, + write: PathList { + name: "write", + paths: &write, + }, + ..ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT) + }; + let mapping = super::plan(&request).unwrap().unwrap(); + let child = &mapping.children[0]; + assert_eq!(child.write, [canonical(&work).join("out")]); + let pinned: Vec = child + .denied_identities + .iter() + .map(Option::is_some) + .collect(); + assert_eq!(pinned, [true, false]); + let pinned: Vec<(&str, bool)> = mapping + .binds + .iter() + .map(|bind| (bind.target.as_str(), bind.identity.is_some())) + .collect(); + assert_eq!( + pinned, + [("/cache", true), ("/src", false), ("/workspace", false)] + ); + // A record keeps every pin of the plan, and narrows writes only to paths that keep its + // read-write mappings writable. + mapping.check(ReservedTargets::CONTAINER_ROOT).unwrap(); + let refused = |change: fn(&mut ExportPlan), expected: &str| { + let mut changed = mapping.clone(); + change(&mut changed); + let error = changed.check(ReservedTargets::CONTAINER_ROOT).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!(error.message().contains(expected), "{error}"); + }; + refused( + |changed| changed.children[0].denied_identities[0] = None, + "does not pin the denied path", + ); + refused( + |changed| changed.binds[0].identity = None, + "does not pin the object that it maps at /cache", + ); + refused( + |changed| changed.children[0].write = vec![changed.children[0].root.join("other")], + "which it maps read-write, stays read-only", + ); + // Without write paths, the guest could move `src/secret` too. + refused( + |changed| changed.children[0].write.clear(), + "does not pin the denied path", + ); + mapping.verify().unwrap(); + fs::rename(&cache, work.join("out/moved")).unwrap(); + fs::create_dir(&cache).unwrap(); + assert_eq!(mapping.verify().unwrap_err().kind(), ErrorKind::Other); + } + + #[test] + fn the_caller_owner_reaches_openvmm_on_linux_hosts() { + let directory = root(); + let work = directory.path().join("work"); + let mappings = [at(&work, "/workspace", false)]; + let request = ExportRequest { + owner: Owner::Caller, + ..ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT) + }; + if cfg!(windows) { + let error = super::plan(&request).unwrap_err(); + assert!(error.message().contains("requires a Linux host"), "{error}"); + return; + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + + let owned = fs::metadata(&work).unwrap(); + if owned.uid() != 0 && owned.gid() != 0 { + let mapping = super::plan(&request).unwrap().unwrap(); + assert_eq!(mapping.owner, Owner::Caller); + let arguments = mapping.openvmm_arguments(); + assert_eq!( + arguments[arguments.len() - 2..], + ["--mount-owner", "caller"] + ); + mapping.verify().unwrap(); + let record = serde_json::to_value(&mapping).unwrap(); + assert_eq!(record["owner"], "caller"); + assert_eq!( + serde_json::from_value::(record).unwrap(), + mapping + ); + } + // Guest root is squashed to the owner, which must not be root. + let system = Path::new("/usr"); + if fs::metadata(system).is_ok_and(|metadata| metadata.uid() == 0) { + let mappings = [at(system, "/srv/system", true)]; + let request = ExportRequest { + owner: Owner::Caller, + ..ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT) + }; + let error = super::plan(&request).unwrap_err(); + assert!( + error + .message() + .contains("must not be owned by UID 0 or GID 0"), + "{error}" + ); + } + } + // The default owner is left out of the record. + let vmm = super::plan(&ExportRequest::new( + &mappings, + ReservedTargets::CONTAINER_ROOT, + )) + .unwrap() + .unwrap(); + assert!(serde_json::to_value(&vmm).unwrap().get("owner").is_none()); + } + + #[test] + fn read_write_mappings_to_which_writes_are_narrowed_stay_visible() { + let directory = root(); + let base = directory.path(); + fs::create_dir_all(base.join("tree/private/public")).unwrap(); + fs::create_dir_all(base.join("tree/out")).unwrap(); + let (tree, private) = (base.join("tree"), base.join("tree/private")); + let denied = [private.clone()]; + let allowed = [private.join("public")]; + let request = |mappings: &[Mapping<'_>], write: &[PathBuf]| { + super::plan(&ExportRequest { + denied: PathList { + name: "denied", + paths: &denied, + }, + allowed: PathList { + name: "allowed", + paths: &allowed, + }, + write: PathList { + name: "write", + paths: write, + }, + ..ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT) + }) + }; + // OpenVMM narrows the writes of the tree to the read-write private, which it cannot hide: + // in a read-only tree, and in a read-write tree whose writes are narrowed. + let out = [tree.join("out")]; + for (mappings, write) in [ + ( + [at(&tree, "/tree", true), at(&private, "/private", false)], + &[][..], + ), + ( + [at(&tree, "/tree", false), at(&private, "/private", false)], + &out[..], + ), + ] { + let error = request(&mappings, write).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error + .message() + .contains("OpenVMM cannot make a hidden path writable"), + "{error}" + ); + } + // In a tree that is writable as a whole, the denied private shows its allowed paths. + let mapping = request( + &[at(&tree, "/tree", false), at(&private, "/private", false)], + &[], + ) + .unwrap() + .unwrap(); + let child = &mapping.children[0]; + assert!(child.writable && child.write.is_empty()); + assert_eq!(child.denied, [canonical(&private)]); + assert_eq!(child.allowed, [canonical(&private).join("public")]); + } + + #[test] + fn denied_mapped_directories_inside_denied_ones_show_their_allowed_paths() { + let directory = root(); + let base = directory.path(); + fs::create_dir_all(base.join("tree/private/public")).unwrap(); + fs::create_dir_all(base.join("tree/other")).unwrap(); + let (tree, private) = (base.join("tree"), base.join("tree/private")); + let denied = [tree.clone(), private.clone()]; + let request = |mappings: &[Mapping<'_>], allowed: &[PathBuf]| { + super::plan(&ExportRequest { + denied: PathList { + name: "denied", + paths: &denied, + }, + allowed: PathList { + name: "allowed", + paths: allowed, + }, + ..ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT) + }) + }; + let both = [at(&tree, "/tree", false), at(&private, "/private", false)]; + // Each mapped directory is denied itself and shows the allowed path inside it; OpenVMM + // needs only the outer denial, which hides the inner one already. + let mapping = request(&both, &[private.join("public")]).unwrap().unwrap(); + let child = &mapping.children[0]; + assert_eq!(child.denied, [canonical(&tree)]); + assert_eq!(child.allowed, [canonical(&private).join("public")]); + mapping.check(ReservedTargets::CONTAINER_ROOT).unwrap(); + // OpenVMM receives each exported directory's policy alone, so one that a denied directory + // outside it contains keeps its own denial. + let mapping = request( + &[at(&private, "/private", false)], + &[private.join("public")], + ) + .unwrap() + .unwrap(); + let child = &mapping.children[0]; + assert_eq!(child.root, canonical(&private)); + assert_eq!(child.denied, [canonical(&private)]); + assert_eq!(child.allowed, [canonical(&private).join("public")]); + mapping.check(ReservedTargets::CONTAINER_ROOT).unwrap(); + // A mapped directory whose denial shows nothing again inside it stays hidden. + let error = request(&both, &[tree.join("other")]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.message().contains("contains the mapped path"), + "{error}" + ); + } + + #[test] + fn recorded_plans_hold_no_more_policy_than_one_aggregate_accepts() { + // Each child's paths of each kind stay within OpenVMM's limits for one directory. + let child = |index: usize| { + let root = std::env::temp_dir().join(format!("nvx-policy-{index}")); + let long = |kind: &str, path: usize| format!("{kind}-{path}-{}", "x".repeat(3990)); + let paths = |kind: &str| -> Vec { + (0..4).map(|path| root.join(long(kind, path))).collect() + }; + // OpenVMM shows paths again only inside a path that it hides. + let hidden = root.join(long("denied", 0)); + ExportChild { + root: root.clone(), + file: false, + writable: true, + denied: paths("denied"), + denied_identities: vec![None; 4], + allowed: (0..4) + .map(|path| hidden.join(format!("allowed-{path}"))) + .collect(), + write: paths("write"), + } + }; + let recorded = |count: usize| ExportPlan { + children: (0..count).map(child).collect(), + binds: (0..count) + .map(|index| Bind { + child: index, + source: String::new(), + target: format!("/c{index}"), + read_only: false, + identity: None, + }) + .collect(), + owner: Owner::Vmm, + }; + recorded(2).check(ReservedTargets::CONTAINER_ROOT).unwrap(); + let error = recorded(3) + .check(ReservedTargets::CONTAINER_ROOT) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.message().contains("exceed its 131072-byte limit"), + "{error}" + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn mapped_paths_resolve_to_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let directory = root(); + let base = directory.path(); + let share = base.join(OsStr::from_bytes(b"share-\xff")); + fs::create_dir(&share).unwrap(); + // A link of a valid name resolves to the same path, which OpenVMM's options could not + // name. + let link = base.join("link"); + std::os::unix::fs::symlink(&share, &link).unwrap(); + for path in [&share, &link] { + let mappings = [at(path, "/workspace", false)]; + let error = super::plan(&ExportRequest::new( + &mappings, + ReservedTargets::CONTAINER_ROOT, + )) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error + .message() + .contains("resolves to a path that is not valid UTF-8"), + "{error}" + ); + } + } } diff --git a/nvx-rs/src/host/launch.rs b/nvx-rs/src/host/launch.rs index ce8927d3c..f2f54035c 100644 --- a/nvx-rs/src/host/launch.rs +++ b/nvx-rs/src/host/launch.rs @@ -401,7 +401,7 @@ fn process_command_line_units(program: &OsStr, arguments: &[OsString]) -> usize mod tests { use super::*; use crate::ErrorKind; - use crate::host::filesystem::{Bind, ExportChild}; + use crate::host::filesystem::{Bind, ExportChild, Owner}; fn spec() -> LaunchSpec { LaunchSpec::new( @@ -473,7 +473,6 @@ mod tests { root: "/host/work".into(), file: false, writable: false, - hidden: false, denied: vec!["/host/work/secret".into()], denied_identities: vec![None], allowed: Vec::new(), @@ -486,6 +485,7 @@ mod tests { read_only: true, identity: None, }], + owner: Owner::Vmm, }); assert_eq!( spec.kernel_command_line().unwrap(), @@ -549,13 +549,13 @@ mod tests { root: "/host/work".into(), file: false, writable: false, - hidden: false, denied_identities: vec![None; denied.len()], denied, allowed: Vec::new(), write: Vec::new(), }], binds: Vec::new(), + owner: Owner::Vmm, }); let error = spec .check_process_command_line("/state/t/control.sock", report) diff --git a/nvx-rs/src/host/sandbox/golden.rs b/nvx-rs/src/host/sandbox/golden.rs index 8f61365bd..687333cf1 100644 --- a/nvx-rs/src/host/sandbox/golden.rs +++ b/nvx-rs/src/host/sandbox/golden.rs @@ -6,19 +6,30 @@ //! The fixtures hold the paths of both `pathlib` flavors, with a fixed current directory, so //! every case runs on every host. The cases that read files run on files of a temporary //! directory. +//! +//! Shares now reach OpenVMM and the guest through the export plan that the planner of every NVX +//! caller makes from resolved host paths, which [`mount::boot_shares`] renders, so no pure function +//! lowers a share anymore. To check that rendering against the records, each case stands in for +//! the plan with [`recorded_plan`]: one child for each share, in order, at the host path and with +//! the policy paths that the retired lowering gave OpenVMM. A launch plans its shares with +//! placeholder host paths, which neither its kernel command line nor its budget reads, and its +//! recorded arguments end with the rendering of the recorded plan. use std::io; +use std::path::PathBuf; use serde_json::{Value, json}; use super::mount::{ - aggregate_child_name, mounts_openvmm_arguments_with, parse_mount_value, validate_mount_target, + POLICY_KINDS, aggregate_child_name, boot_shares, parse_mount_value, validate_mount_target, + validate_mounts, }; use super::options::{ PolicyPathOption, SandboxOperation, SandboxOptions, sandbox_mounts_with, validate_exec_environment, validate_run_share_targets, validate_sandbox_systemd_policy, }; use super::*; +use crate::host::filesystem::{Bind, ExportChild, Owner}; use crate::host::python::golden::{check_section, fixture, flavor, hex, text, texts}; /// Returns the message of a result's error, or `None` if it succeeded. @@ -67,6 +78,75 @@ fn parse_mount(flavor: Flavor, spec: &Value) -> Result { ) } +/// Returns the export plan whose rendering gives OpenVMM the arguments that the retired lowering +/// of `mounts` gave it: each share is a child of its own, in order, bound at its root, with its +/// host path as given and, unless `placeholders`, its policy paths, joined to the absolute host +/// path, as `cwd` makes it, in an aggregate, and as given for one directory. +/// +/// With `placeholders`, the host paths are absolute paths of this host, and the plan has no +/// policy paths, so that a [`SandboxPlan`] takes it. +fn recorded_plan( + mounts: &[SandboxMount], + cwd: &dyn Fn() -> io::Result, + placeholders: bool, +) -> Result { + let aggregate = mounts.len() > 1 || mounts.iter().any(|mount| mount.kind() == MountKind::File); + let mut children = Vec::with_capacity(mounts.len()); + let mut binds = Vec::with_capacity(mounts.len()); + for (index, mount) in mounts.iter().enumerate() { + let policy: Vec<(PolicyKind, Vec)> = if placeholders { + Vec::new() + } else if aggregate { + mount.absolute_policy_paths_with(cwd)? + } else { + POLICY_KINDS + .iter() + .map(|&kind| (kind, mount.policy().paths(kind).to_vec())) + .collect() + }; + let paths = |wanted: PolicyKind| -> Vec { + policy + .iter() + .filter(|(kind, _)| *kind == wanted) + .flat_map(|(_, paths)| paths.iter().map(PathBuf::from)) + .collect() + }; + let denied = paths(PolicyKind::Denied); + children.push(ExportChild { + root: if placeholders { + super::tests::host(&format!("/host{index}")) + } else { + mount.host_path().to_path_buf() + }, + file: mount.kind() == MountKind::File, + writable: mount.access() == MountAccess::ReadWrite, + denied_identities: vec![None; denied.len()], + denied, + allowed: paths(PolicyKind::Allowed), + write: paths(PolicyKind::Writable), + }); + binds.push(Bind { + child: index, + source: String::new(), + target: mount.guest_target().to_owned(), + read_only: mount.access() == MountAccess::ReadOnly, + identity: None, + }); + } + binds.sort_by(|left, right| { + (left.target.matches('/').count(), &left.target) + .cmp(&(right.target.matches('/').count(), &right.target)) + }); + Ok(ExportPlan { + children, + binds, + owner: match mounts.first().map(SandboxMount::owner) { + Some(MountOwner::Caller) => Owner::Caller, + _ => Owner::Vmm, + }, + }) +} + /// Returns the expected outcome of a case: its `key` field, or its `error` field. fn outcome(case: &Value, key: &str) -> std::result::Result { match case.get("error") { @@ -137,11 +217,21 @@ fn mounts_lower_to_the_arguments_that_python_gives() { check_section(&fixture, "mounts", |mismatches, case| { let flavor = flavor(case); let cwd = cwd(&fixture, flavor); + // The record gives every share the fragment of a directory share of its own, which a + // file never used: OpenVMM appends the fragment of an aggregate for a file. + let fragment = match case["mount"]["access"].as_str() { + Some(access) if case["kind"] == "file" => json!( + MountAccess::from_name(access) + .map(|access| mount::aggregate_command_line_fragment(&[access])) + .ok() + ), + _ => case["fragment"].clone(), + }; let expected = outcome(case, "mount").map(|_| { json!([ case["mount"], case["arguments"], - case["fragment"], + fragment, case["absolute"], case["absolute_policy"], ]) @@ -152,10 +242,11 @@ fn mounts_lower_to_the_arguments_that_python_gives() { .into_iter() .map(|(kind, paths)| json!([kind.as_str(), paths])) .collect(); + let boot = boot_shares(&recorded_plan(std::slice::from_ref(&mount), &cwd, false)?)?; Ok(json!([ mount_fields(&mount), - mounts_openvmm_arguments_with(std::slice::from_ref(&mount), &cwd)?, - mount.command_line_fragment(), + boot.arguments, + boot.fragment, mount_fields(&mount.absolute_with(&cwd)?), policy, ])) @@ -198,7 +289,6 @@ fn launches_give_the_arguments_and_command_lines_that_python_gives() { workload_identity: (id(0), id(1)), memory_max: limit("memory_max"), pids_max: limit("pids_max"), - mounts, }; let expected = match (case.get("error"), case.get("kernel_error")) { (Some(message), _) => Err(message.as_str().unwrap().to_owned()), @@ -208,11 +298,22 @@ fn launches_give_the_arguments_and_command_lines_that_python_gives() { kernel_error.unwrap_or(&Value::Null), ])), }; + let cwd = cwd(&fixture, flavor); let actual = SandboxLaunch::new_in(flavor, layers, text(case, "scratch"), settings) .and_then(|launch| { - let arguments = launch - .plan() - .openvmm_arguments_with(&cwd(&fixture, flavor))?; + validate_mounts(&mounts)?; + let export = if mounts.is_empty() { + None + } else { + Some(recorded_plan(&mounts, &cwd, true)?) + }; + let launch = launch.with_export(export)?; + let mut arguments = launch.plan().openvmm_arguments()?; + if let Some(export) = launch.export() { + let placeholders = boot_shares(export)?.arguments.len(); + arguments.truncate(arguments.len() - placeholders); + arguments.extend(boot_shares(&recorded_plan(&mounts, &cwd, false)?)?.arguments); + } let command_line = launch .plan() .kernel_command_line(text(case, "command_line")); diff --git a/nvx-rs/src/host/sandbox/mod.rs b/nvx-rs/src/host/sandbox/mod.rs index 7571bd71b..9d4e05a95 100644 --- a/nvx-rs/src/host/sandbox/mod.rs +++ b/nvx-rs/src/host/sandbox/mod.rs @@ -7,18 +7,22 @@ //! //! - [`plan`]: the typed, persistable [`SandboxPlan`] of a cold layered launch: one read-only //! `distro` block device, an EROFS filesystem or a GPT disk with the identity that the guest -//! expects of it, over an upper on an ext4 scratch image or in RAM, with the launch settings and -//! optionally a network, a [`SandboxNetwork`] whose typed policy may be proxy-only. It renders -//! the OpenVMM arguments, [`openvmm_arguments`](SandboxPlan::openvmm_arguments), and the kernel -//! command line, [`kernel_command_line`](SandboxPlan::kernel_command_line), whose boot tokens +//! expects of it, over an upper on an ext4 scratch image or in RAM, with the launch settings, the +//! export plan of its live shares, and optionally a network, a [`SandboxNetwork`] whose typed +//! policy may be proxy-only. It renders the OpenVMM arguments, +//! [`openvmm_arguments`](SandboxPlan::openvmm_arguments), and the kernel command line, +//! [`kernel_command_line`](SandboxPlan::kernel_command_line), whose boot tokens //! [`protocol::root`](crate::protocol::root) defines. //! - [`SandboxLayer`]: the EROFS distro layer of `nvx.py sandbox`, `ROLE,PATH,EROFS_UUID`, the //! one [`LayerRole`] that a sandbox takes. -//! - [`SandboxMount`]: a live share of a host directory or regular file at a guest target, with -//! its access mode, owner, and access-policy paths; [`mount`] holds the rules of guest targets, -//! the aggregate of several shares, and the checks of host paths. +//! - [`SandboxMount`]: a requested live share of a host directory or regular file at a guest +//! target, with its access mode, owner, and access-policy paths; [`mount`] holds the rules of +//! guest targets and the checks of host paths, plans the shares of a sandbox through the planner +//! of every NVX caller, [`host::filesystem`](crate::host::filesystem), and renders the plan as +//! the guest's shell reads it at boot. //! - [`SandboxLaunch`]: the launch of `nvx.py sandbox`, its layers over its scratch image with -//! its settings, which plans its EROFS distro layer over the scratch image. +//! its settings and the export plan of its shares, which plans its EROFS distro layer over the +//! scratch image. //! - [`options`]: the checks that `nvx.py sandbox` and `nvx.py run` apply to their options: the //! attribution of access-policy paths to shares, the systemd entrypoint check and the preflight //! that verifies the distro layer, the targets of `nvx.py run` shares, and the managed exec @@ -63,6 +67,7 @@ pub use self::plan::{ }; pub use self::prepared::PreparedLaunch; use crate::error::{Error, Result}; +use crate::host::filesystem::ExportPlan; pub use crate::host::python::builtins::{Int, IntError}; use crate::host::python::builtins::{is_space, repr}; use crate::host::python::fs; @@ -168,8 +173,8 @@ fn reject_container_path(path: &PurePath, shown: &str) -> Result<()> { Ok(()) } -/// The launch settings of a [`SandboxPlan`] or a [`SandboxLaunch`], besides its block devices, with -/// the defaults of `nvx.py sandbox`. +/// The launch settings of a [`SandboxPlan`] or a [`SandboxLaunch`], besides its block devices and +/// its shares, with the defaults of `nvx.py sandbox`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SandboxSettings { /// The absolute path of the workload's program in the container, without whitespace. @@ -185,8 +190,6 @@ pub struct SandboxSettings { /// The most processes that the workload may run, if positive; the guest's own init counts /// too, so its `pids.max` is one more. pub pids_max: Option, - /// The live shares, in child order. - pub mounts: Vec, } impl Default for SandboxSettings { @@ -198,15 +201,13 @@ impl Default for SandboxSettings { workload_identity: DEFAULT_WORKLOAD_IDENTITY, memory_max: None, pids_max: None, - mounts: Vec::new(), } } } /// Checks the launch settings of a plan, in this order: the entrypoint, the arguments, the -/// hostname, a non-root identity, positive limits, limits that the guest's cgroups can apply, at -/// most [`u64::MAX`] bytes of memory and 4,194,303 processes, and shares whose targets do not -/// overlap and that have one owner. +/// hostname, a non-root identity, positive limits, limits that the guest's cgroups can apply, and +/// at most [`u64::MAX`] bytes of memory and 4,194,303 processes. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). fn validate_settings(settings: &SandboxSettings) -> Result<()> { @@ -217,7 +218,6 @@ fn validate_settings(settings: &SandboxSettings) -> Result<()> { workload_identity: (uid, gid), memory_max, pids_max, - mounts, } = settings; let has_invalid_chars = |text: &str| text.contains('"') || text.bytes().any(|byte| byte == 0xa0); @@ -267,11 +267,12 @@ fn validate_settings(settings: &SandboxSettings) -> Result<()> { "--pids-max must be at most {PIDS_MAX_LIMIT}" ))); } - mount::validate_mounts(mounts) + Ok(()) } /// The launch of a sandbox of `nvx.py sandbox`: its read-only layers, its ext4 scratch image, its -/// settings, and its network, which it plans as an EROFS distro layer over the scratch image. +/// settings, the export plan of its shares, and its network, which it plans as an EROFS distro +/// layer over the scratch image. /// /// Its layers are keyed by the role of each `--layer` value, of which a launch takes only the /// distro layer. [`plan`](Self::plan) returns its [`SandboxPlan`], which renders its OpenVMM @@ -283,13 +284,14 @@ pub struct SandboxLaunch { /// The read-only layers, in the order given: the distro layer alone. layers: Vec, /// The plan: the distro layer, an EROFS image with its UUID, over the scratch image, with the - /// settings. + /// settings and the export plan. plan: SandboxPlan, } impl SandboxLaunch { /// Returns the launch of `layers` over the ext4 image at `scratch`, which is normalized as - /// `pathlib.Path` normalizes it. + /// `pathlib.Path` normalizes it, without shares, which [`with_export`](Self::with_export) + /// adds. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), checking in this /// order: no duplicate roles, exactly one layer, the distro layer, a UUID of the distro layer @@ -368,9 +370,21 @@ impl SandboxLaunch { self.plan.settings() } - /// Returns the shares. - pub fn mounts(&self) -> &[SandboxMount] { - self.plan.mounts() + /// Returns the export plan of the shares, if any. + pub fn export(&self) -> Option<&ExportPlan> { + self.plan.export() + } + + /// Returns the launch with `export`, the export plan of its live shares, if any, such as + /// [`mount::plan_mounts`] makes. + /// + /// Fails as [`SandboxPlan::with_export`] checks the export plan. + pub fn with_export(self, export: Option) -> Result { + let Self { layers, plan } = self; + Ok(Self { + layers, + plan: plan.with_export(export)?, + }) } /// Returns the launch with `network`, or with no network device for `None`. @@ -388,7 +402,8 @@ impl SandboxLaunch { } /// Returns the plan of the launch: its distro layer, an EROFS image with the UUID of its - /// `--layer` value, over its scratch image, with its settings and its network. + /// `--layer` value, over its scratch image, with its settings, the export plan of its shares, + /// and its network. pub fn plan(&self) -> &SandboxPlan { &self.plan } @@ -408,9 +423,9 @@ impl SandboxLaunch { /// Checks the launch against the host, as [`SandboxPlan::validated`] checks its plan: the /// distro layer and the scratch image must be regular files whose paths have no comma or /// semicolon, and whose names, and those of the files to which their paths resolve, do not end - /// in an extension with which OpenVMM would not attach them as raw images, each share must - /// pass [`SandboxMount::validated`], and their host paths - /// [`mount::validate_mount_host_paths`]. + /// in an extension with which OpenVMM would not attach them as raw images, and the export + /// plan's directories and files must still not overlap, and it must pass + /// [`ExportPlan::verify`]. /// /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) for a missing image, /// and otherwise as those checks do. diff --git a/nvx-rs/src/host/sandbox/mount.rs b/nvx-rs/src/host/sandbox/mount.rs index 25b1d13fa..e4530c178 100644 --- a/nvx-rs/src/host/sandbox/mount.rs +++ b/nvx-rs/src/host/sandbox/mount.rs @@ -1,26 +1,34 @@ //! Live virtio-fs shares of host directories and files inside the sandbox's container rootfs. //! -//! OpenVMM attaches one virtio-fs device, with the tag [`MOUNT_TAG`]. One directory share uses -//! it directly, and the guest mounts it at its target. Several shares, or a file, share it as the +//! A [`SandboxMount`] is one `--mount` request: a host directory or file at a guest target, with +//! its access mode, owner, and access-policy paths. [`plan_mounts`] plans the shares of a sandbox +//! through the planner of every NVX caller, [`filesystem::plan`], for the container root, whose +//! runtime keeps the [`RESERVED_MOUNT_TARGETS`] and the [`RESERVED_EXACT_MOUNT_TARGETS`], and the +//! sandbox records the resulting [`ExportPlan`]. +//! +//! [`boot_shares`] renders such a plan in the form that the guest's shell reads at boot. OpenVMM +//! attaches one virtio-fs device, with the tag [`MOUNT_TAG`]. One directory uses it directly, and +//! the guest mounts it at its target. Several directories or files, or a file, share it as the //! children of an aggregate, each named by [`aggregate_child_name`], which the guest mounts at //! [`AGGREGATE_MOUNT_TARGET`], a directory that only the guest's root can enter, before it binds -//! each child at its target, as the `nvx_share=` kernel parameters of [`aggregate_share_tokens`] -//! tell it. +//! each child at its target, as the `nvx_share=` kernel parameters tell it. //! //! A share's access-policy paths are host paths: `--mount-deny` hides a path, `--mount-allow` //! exposes a path inside a denied path again, and `--mount-write` makes a path one of the only -//! writable parts of a read-write share. OpenVMM resolves relative paths in the only share, and -//! attributes absolute paths to the child that contains them. +//! writable parts of a read-write share. A relative path names a path in its share, and each +//! must lie in the host directory of its share. use std::fmt; use std::io; -use std::path::Path; +use std::path::{Path, PathBuf}; use sha2::{Digest, Sha256}; use super::refused; use crate::error::{Error, Result}; -use crate::host::filesystem::within; +use crate::host::filesystem::{ + self, ExportPlan, ExportRequest, Mapping, Owner, PathList, ReservedTargets, Target, within, +}; use crate::host::python::builtins::{is_space, repr}; use crate::host::python::fs; use crate::host::python::path::{Flavor, PurePath}; @@ -571,20 +579,12 @@ impl SandboxMount { pub fn validated(&self) -> Result<()> { require_mount_owner_supported(self.owner)?; let host_path = self.host_path.as_path(); - match self.kind { - MountKind::File if fs::is_symlink(host_path) || !fs::is_file(host_path) => { - return Err(refused(format!( - "sandbox mount host path is not a regular file: {}", - self.host_path - ))); - } - MountKind::Directory if fs::is_symlink(host_path) || !fs::is_dir(host_path) => { - return Err(refused(format!( - "sandbox mount host path is not a plain directory: {}", - self.host_path - ))); - } - _ => {} + let kind_matches = match self.kind { + MountKind::File => fs::is_file(host_path), + MountKind::Directory => fs::is_dir(host_path), + }; + if fs::is_symlink(host_path) || !kind_matches { + return Err(self.kind_refusal()); } if self.owner == MountOwner::Caller && !cfg!(windows) { let status = fs::stat(host_path) @@ -600,6 +600,20 @@ impl SandboxMount { Ok(()) } + /// Returns the error for a host path that is not of the share's kind. + fn kind_refusal(&self) -> Error { + refused(match self.kind { + MountKind::File => format!( + "sandbox mount host path is not a regular file: {}", + self.host_path + ), + MountKind::Directory => format!( + "sandbox mount host path is not a plain directory: {}", + self.host_path + ), + }) + } + /// Returns the share with its host path made absolute by joining it to the current /// directory, without normalizing it, so OpenVMM still sees, and refuses, every /// symbolic-link component of the path that was requested. @@ -658,12 +672,6 @@ impl SandboxMount { }) .collect()) } - - /// Returns the bootstrap tokens that OpenVMM appends to the kernel command line for this - /// share alone: its target, the device tag, and its mode. - pub fn command_line_fragment(&self) -> String { - command_line_fragment(&self.guest_target, self.access) - } } /// Returns the bootstrap tokens of a share at `target` with `access`. @@ -672,13 +680,6 @@ fn command_line_fragment(target: &str, access: MountAccess) -> String { format!("{directory}{target}{tag}{MOUNT_TAG}{mode}{access}") } -/// Returns whether `mounts` share the virtio-fs device as the children of an aggregate: several -/// shares do, and so does a file, because the guest mounts the device's root, which is a -/// directory. -pub fn mounts_use_aggregate(mounts: &[SandboxMount]) -> bool { - mounts.len() > 1 || mounts.iter().any(|mount| mount.kind == MountKind::File) -} - /// Checks the live shares that one sandbox attaches, in child order: no target may overlap /// another, so that no share hides another, and every share has the same owner. /// @@ -729,9 +730,8 @@ impl HostQueries<'_> { /// follows symbolic links but not bind mounts, if one has the file identity of the other or of /// one of its ancestors, as a hard link or a path inside a bind mount of it does. A shared file /// therefore overlaps a shared directory that contains it, or another share of the same file, -/// but not a file beside it. With several shares, each policy path must name a path inside the -/// host directory of its share, and only a denied path may name the share itself. One share has -/// nothing to compare. +/// but not a file beside it. Each policy path must name a path inside the host directory of its +/// share, and only a denied path may name the share itself. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or /// [`ErrorKind::Io`](crate::ErrorKind::Io) if a path cannot be resolved or examined. @@ -744,9 +744,6 @@ pub(crate) fn validate_mount_host_paths_with( mounts: &[SandboxMount], host: &HostQueries<'_>, ) -> Result<()> { - if mounts.len() < 2 { - return Ok(()); - } let roots = mounts .iter() .map(|mount| { @@ -754,6 +751,67 @@ pub(crate) fn validate_mount_host_paths_with( .map_err(|error| os_error(&error, Some(mount.host_path.as_str()))) }) .collect::>>()?; + if mounts.len() > 1 { + check_distinct_host_paths(&roots, host)?; + } + for (mount, root) in mounts.iter().zip(&roots) { + for (kind, paths) in mount.absolute_policy_paths_with(host.cwd)? { + let option = kind.option(); + for path in paths { + let pure = PurePath::new(root.flavor(), &path); + let resolved = + (host.resolve)(&pure).map_err(|error| os_error(&error, Some(pure.as_str())))?; + // A denied root hides the share behind its allowed paths, which OpenVMM requires; + // it accepts no other policy path at a root. + if resolved.parents().any(|parent| parent == *root) + || (kind == PolicyKind::Denied && resolved == *root) + { + continue; + } + let several = if mounts.len() > 1 { + format!( + "; with several shares, each {option} follows the --mount whose \ + directory it names" + ) + } else { + String::new() + }; + return Err(refused(format!( + "{option} {path} is not inside the host directory of --mount {}{several}", + mount.guest_target + ))); + } + } + } + Ok(()) +} + +/// Refuses directories and files of `export` that came to overlap since it was planned: one that +/// a hard link or bind mount made the same object as another, or put inside another, so that a +/// share could reach the files of another under its access mode. [`plan_mounts`] refused such +/// host paths when it planned them, and a launch checks them again, since the host may have +/// changed meanwhile. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or +/// [`ErrorKind::Io`](crate::ErrorKind::Io) if a path cannot be examined. +pub(crate) fn check_distinct_export_roots( + export: &ExportPlan, + host: &HostQueries<'_>, +) -> Result<()> { + if export.children.len() < 2 { + return Ok(()); + } + let roots = export + .children + .iter() + .map(|child| Ok(PurePath::native(path_text(&child.root)?))) + .collect::>>()?; + check_distinct_host_paths(&roots, host) +} + +/// Refuses resolved share roots that overlap, by path or by the file identity of a root and its +/// ancestors. +fn check_distinct_host_paths(roots: &[PurePath], host: &HostQueries<'_>) -> Result<()> { // The identity of each root, then of each of its ancestors. let lineages = roots .iter() @@ -787,28 +845,6 @@ pub(crate) fn validate_mount_host_paths_with( } } } - for (mount, root) in mounts.iter().zip(&roots) { - for (kind, paths) in mount.absolute_policy_paths_with(host.cwd)? { - let option = kind.option(); - for path in paths { - let pure = PurePath::new(root.flavor(), &path); - let resolved = - (host.resolve)(&pure).map_err(|error| os_error(&error, Some(pure.as_str())))?; - // A denied root hides the share behind its allowed paths, which OpenVMM requires; - // it accepts no other policy path at a root. - if resolved.parents().any(|parent| parent == *root) - || (kind == PolicyKind::Denied && resolved == *root) - { - continue; - } - return Err(refused(format!( - "{option} {path} is not inside the host directory of --mount {}; with several \ - shares, each {option} follows the --mount whose directory it names", - mount.guest_target - ))); - } - } - } Ok(()) } @@ -846,68 +882,235 @@ pub fn mount_argument(target: &str, host_path: &str, access: MountAccess) -> Str format!("{target},{host_path},{access}") } -/// Returns the OpenVMM arguments that attach `mounts`: one directory share directly, and -/// several shares, or a file, as the children of an aggregate, in order. Then come the -/// access-policy paths of each share, absolute for an aggregate, and the owner unless it is -/// `vmm`. +/// Plans the export of `mounts`, the live shares of one sandbox, through the planner of every +/// NVX caller, [`filesystem::plan`], after it checks them with [`validate_mounts`], each share +/// with [`SandboxMount::validated`], and their host paths with [`validate_mount_host_paths`]. /// -/// Fails as [`SandboxMount::absolute`] does. -pub fn mounts_openvmm_arguments(mounts: &[SandboxMount]) -> Result> { - mounts_openvmm_arguments_with(mounts, &fs::cwd) +/// Each share maps its host path at its guest target, read-only or read-write, and its denied, +/// allowed, and writable paths, joined to its absolute host path when relative, become the +/// export's denied, allowed, and write paths. The owner of the shares, which [`validate_mounts`] +/// requires to be one, becomes the export's, and the container root reserves the trees of its +/// runtime. Each share keeps the kind with which it was requested: a host path that the planner +/// finds to be of the other kind is refused, as [`SandboxMount::validated`] refuses it. Returns +/// `None` without shares. +/// +/// Fails as those checks, [`SandboxMount::absolute`], and [`filesystem::plan`] do. +pub fn plan_mounts(mounts: &[SandboxMount]) -> Result> { + validate_mounts(mounts)?; + for mount in mounts { + mount.validated()?; + } + validate_mount_host_paths(mounts)?; + plan_mounts_with(mounts, &fs::cwd) } -/// Implements [`mounts_openvmm_arguments`] with `cwd` as `Path.cwd()`. -pub(crate) fn mounts_openvmm_arguments_with( +/// Plans the export of `mounts` as [`plan_mounts`] does, without checking them against the host +/// first, with `cwd` as `Path.cwd()`. +pub(crate) fn plan_mounts_with( mounts: &[SandboxMount], cwd: &dyn Fn() -> io::Result, -) -> Result> { - let mut arguments = Vec::new(); - let aggregate = mounts_use_aggregate(mounts); - match mounts { - [] => {} - [mount] if !aggregate => arguments.extend([ - "--mount".to_owned(), - mount_argument(&mount.guest_target, mount.host_path.as_str(), mount.access), - ]), +) -> Result> { + let Some(first) = mounts.first() else { + return Ok(None); + }; + let mut hosts = Vec::with_capacity(mounts.len()); + let (mut denied, mut allowed, mut write) = (Vec::new(), Vec::new(), Vec::new()); + for mount in mounts { + hosts.push(mount.absolute_with(cwd)?.host_path.as_path().to_path_buf()); + for (kind, paths) in mount.absolute_policy_paths_with(cwd)? { + match kind { + PolicyKind::Denied => &mut denied, + PolicyKind::Allowed => &mut allowed, + PolicyKind::Writable => &mut write, + } + .extend(paths.into_iter().map(PathBuf::from)); + } + } + let mappings: Vec> = mounts + .iter() + .zip(&hosts) + .map(|(mount, host)| Mapping { + name: "--mount", + path: host, + target: Target::Guest(&mount.guest_target), + read_only: mount.access == MountAccess::ReadOnly, + }) + .collect(); + let list = |kind: PolicyKind, paths| PathList { + name: kind.option(), + paths, + }; + let plan = filesystem::plan(&ExportRequest { + mappings: &mappings, + denied: list(PolicyKind::Denied, &denied), + allowed: list(PolicyKind::Allowed, &allowed), + write: list(PolicyKind::Writable, &write), + owner: match first.owner { + MountOwner::Vmm => Owner::Vmm, + MountOwner::Caller => Owner::Caller, + }, + reserved: ReservedTargets::CONTAINER_ROOT, + })?; + // The planner reads each host path's kind again; a directory that replaced a shared file + // since the share was requested is refused rather than shared whole, and so is a host path + // that now leads into another share's directory, where the share would have no root of its + // own. + if let Some(plan) = &plan { + for mount in mounts { + let Some(bind) = plan + .binds + .iter() + .find(|bind| bind.target == mount.guest_target && bind.source.is_empty()) + else { + return Err(refused(format!( + "sandbox mount host path {} lies inside the host directory of another share; \ + a share could reach files of another under its access mode", + mount.host_path + ))); + }; + let file = plan.children.get(bind.child).map(|child| child.file); + if file != Some(mount.kind == MountKind::File) { + return Err(mount.kind_refusal()); + } + } + } + Ok(plan) +} + +/// The form in which the guest's shell receives an export plan at boot: OpenVMM's arguments that +/// attach its directories and files, the `nvx_share=` kernel parameters that tell the guest where +/// to bind the children of an aggregate, and the bootstrap tokens that OpenVMM appends to the +/// kernel command line. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct BootShares { + /// OpenVMM's arguments: one directory share or an aggregate with its children, in order, the + /// access-policy paths of each, absolute, and the owner unless it is `vmm`. + pub arguments: Vec, + /// The `nvx_share=` kernel parameters of an aggregate, one for each child, in order; none for + /// one directory share. + pub tokens: Vec, + /// The bootstrap tokens that OpenVMM appends after the kernel command line. + pub fragment: String, +} + +/// Renders `plan` in the form that the guest's shell reads at boot. +/// +/// The shell binds each exported directory or file at one target, so every child of the plan +/// must be mapped exactly once, at its root, with the access with which OpenVMM exports it, at a +/// target that [`validate_mount_target`] accepts. +/// One directory then uses OpenVMM's virtio-fs device directly, and anything else shares it as +/// the children of an aggregate, named by [`aggregate_child_name`]. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a plan that the boot +/// tokens cannot express, such as one that maps a path below a share's root, rather than extend +/// what the shell reads. +pub fn boot_shares(plan: &ExportPlan) -> Result { + let mut shares = Vec::with_capacity(plan.children.len()); + for (index, child) in plan.children.iter().enumerate() { + let root = path_text(&child.root)?; + let binds: Vec<_> = plan + .binds + .iter() + .filter(|bind| bind.child == index) + .collect(); + let bind = match binds[..] { + [bind] if bind.source.is_empty() => bind, + [bind] => { + return Err(refused(format!( + "the boot tokens of a share cannot map {}, a path below the share's root {root}", + child.root.join(&bind.source).display() + ))); + } + _ => { + return Err(refused(format!( + "the boot tokens of a share map {root} at one target, not {}", + binds.len() + ))); + } + }; + validate_mount_target(&bind.target)?; + // The tokens mount a share with the access with which OpenVMM exports it, so a bind of + // another access, such as a read-only bind of a directory whose writes OpenVMM narrows to + // a path inside it, would let the guest write what the plan keeps read-only. + if bind.read_only == child.writable { + let (bound, exported) = if child.writable { + ("read-only", "read-write") + } else { + ("read-write", "read-only") + }; + return Err(refused(format!( + "the boot tokens of a share cannot map {root} {bound} while OpenVMM exports it \ + {exported}" + ))); + } + let access = if child.writable { + MountAccess::ReadWrite + } else { + MountAccess::ReadOnly + }; + shares.push((child, root, bind.target.as_str(), access)); + } + if plan.binds.len() != shares.len() { + return Err(refused( + "the boot tokens of a share cannot map a path of an unknown host directory", + )); + } + let mut boot = BootShares::default(); + match shares[..] { + [] => return Ok(boot), + [(child, root, target, access)] if !child.file => { + boot.arguments = vec!["--mount".to_owned(), mount_argument(target, root, access)]; + boot.fragment = command_line_fragment(target, access); + } _ => { - arguments.extend([ + boot.arguments = vec![ "--mount-aggregate".to_owned(), AGGREGATE_MOUNT_TARGET.to_owned(), - ]); - for (index, mount) in mounts.iter().enumerate() { - let name = aggregate_child_name(index, &mount.guest_target); - arguments.push("--mount-child".to_owned()); - arguments.push(aggregate_child_argument( - &name, - mount.host_path.as_str(), - mount.access, - mount.kind, - )); + ]; + for (index, &(child, root, target, access)) in shares.iter().enumerate() { + let name = aggregate_child_name(index, target); + let (kind, file) = if child.file { + (MountKind::File, ",file") + } else { + (MountKind::Directory, "") + }; + boot.arguments.push("--mount-child".to_owned()); + boot.arguments + .push(aggregate_child_argument(&name, root, access, kind)); + boot.tokens + .push(format!("nvx_share={name},{target},{access}{file}")); } + let accesses: Vec = + shares.iter().map(|&(_, _, _, access)| access).collect(); + boot.fragment = aggregate_command_line_fragment(&accesses); } } - for mount in mounts { - let policy = if aggregate { - mount.absolute_policy_paths_with(cwd)? - } else { - POLICY_KINDS - .iter() - .map(|&kind| (kind, mount.policy.paths(kind).to_vec())) - .collect() - }; - for (kind, paths) in policy { + for &(child, ..) in &shares { + for (kind, paths) in [ + (PolicyKind::Denied, &child.denied), + (PolicyKind::Allowed, &child.allowed), + (PolicyKind::Writable, &child.write), + ] { for path in paths { - arguments.push(kind.option().to_owned()); - arguments.push(path); + boot.arguments.push(kind.option().to_owned()); + boot.arguments.push(path_text(path)?.to_owned()); } } } - if let Some(first) = mounts.first() - && first.owner != MountOwner::Vmm - { - arguments.extend(["--mount-owner".to_owned(), first.owner.as_str().to_owned()]); + if plan.owner == Owner::Caller { + boot.arguments.push("--mount-owner".to_owned()); + boot.arguments.push(MountOwner::Caller.as_str().to_owned()); } - Ok(arguments) + Ok(boot) +} + +/// Returns the text of a host path of an export plan, which OpenVMM's options carry. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a path that is not +/// valid UTF-8. +fn path_text(path: &Path) -> Result<&str> { + path.to_str() + .ok_or_else(|| refused(format!("{} is not valid UTF-8", path.display()))) } /// Returns the kernel command-line tokens that tell the guest where to bind each child of an @@ -938,16 +1141,5 @@ pub fn aggregate_command_line_fragment(accesses: &[MountAccess]) -> String { command_line_fragment(AGGREGATE_MOUNT_TARGET, access) + AGGREGATE_COMMAND_LINE_SUFFIX } -/// Returns the bootstrap tokens that OpenVMM appends for `mounts`. -pub fn mounts_command_line_fragment(mounts: &[SandboxMount]) -> String { - match mounts { - [] => String::new(), - [mount] if !mounts_use_aggregate(mounts) => mount.command_line_fragment(), - _ => aggregate_command_line_fragment( - &mounts.iter().map(|mount| mount.access).collect::>(), - ), - } -} - #[cfg(test)] mod tests; diff --git a/nvx-rs/src/host/sandbox/mount/tests.rs b/nvx-rs/src/host/sandbox/mount/tests.rs index cdb3b7b14..fb67b2050 100644 --- a/nvx-rs/src/host/sandbox/mount/tests.rs +++ b/nvx-rs/src/host/sandbox/mount/tests.rs @@ -4,8 +4,9 @@ use std::fs; use super::*; use crate::ErrorKind; +use crate::host::filesystem::Bind; use crate::host::python::fs::cwd; -use crate::host::sandbox::tests::{native, policy}; +use crate::host::sandbox::tests::{deny, exported, host, host_text, native, policy}; fn parse(value: &str, policy: MountPolicy) -> Result { SandboxMount::parse(value, policy, MountOwner::Vmm, MountKind::Directory) @@ -32,19 +33,29 @@ fn mount_parser_defaults_to_read_only_and_accepts_rw() { .unwrap(); assert_eq!(writable.access(), MountAccess::ReadWrite); assert_eq!(writable.policy().denied, ["logs", "secrets"]); + // One directory uses the device directly, with the absolute denied paths of its plan. + let mut plan = exported(&[( + "/opt/hostedtoolcache", + "/host/dir", + MountAccess::ReadWrite, + MountKind::Directory, + )]); + deny(&mut plan, 0, &["/host/dir/logs", "/host/dir/secrets"]); + let boot = boot_shares(&plan).unwrap(); assert_eq!( - mounts_openvmm_arguments(std::slice::from_ref(&writable)).unwrap(), + boot.arguments, [ - "--mount", - "/opt/hostedtoolcache,host-dir,rw", - "--mount-deny", - "logs", - "--mount-deny", - "secrets", + "--mount".to_owned(), + format!("/opt/hostedtoolcache,{},rw", host_text("/host/dir")), + "--mount-deny".to_owned(), + host_text("/host/dir/logs"), + "--mount-deny".to_owned(), + host_text("/host/dir/secrets"), ] ); + assert!(boot.tokens.is_empty()); assert_eq!( - writable.command_line_fragment(), + boot.fragment, " virtfs_dir=/opt/hostedtoolcache virtfs_tag=microvm virtfs_mode=rw" ); } @@ -150,19 +161,33 @@ fn mount_parser_accepts_host_paths_with_commas() { assert_eq!(mount.access(), access); // OpenVMM finds the end of the host path before the mode, which NVX therefore always // gives. + let mut plan = exported(&[(target, "/unused", access, MountKind::Directory)]); + plan.children[0].root = PathBuf::from(native(host_path)); assert_eq!( - mounts_openvmm_arguments(&[mount]).unwrap(), + boot_shares(&plan).unwrap().arguments, [ "--mount".to_owned(), format!("{target},{},{access}", native(host_path)), - "--mount-deny".to_owned(), - "secret".to_owned(), ] ); } - let alpha_mount = parse(&format!("/work,{alpha},rw"), MountPolicy::default()).unwrap(); - let reports_mount = parse(&format!("/reports,{reports},ro"), MountPolicy::default()).unwrap(); - let arguments = mounts_openvmm_arguments(&[alpha_mount, reports_mount]).unwrap(); + let mut plan = exported(&[ + ( + "/work", + "/unused/a", + MountAccess::ReadWrite, + MountKind::Directory, + ), + ( + "/reports", + "/unused/b", + MountAccess::ReadOnly, + MountKind::Directory, + ), + ]); + plan.children[0].root = PathBuf::from(native(alpha)); + plan.children[1].root = PathBuf::from(native(reports)); + let arguments = boot_shares(&plan).unwrap().arguments; let children: Vec<&String> = arguments .iter() .zip(arguments.iter().skip(1)) @@ -186,9 +211,16 @@ fn mount_parser_accepts_host_paths_with_commas() { fn mount_owner_defaults_to_vmm_and_forwards_caller() { let default = parse("/workspace,host-dir,rw", MountPolicy::default()).unwrap(); assert_eq!(default.owner(), MountOwner::Vmm); + let mut plan = exported(&[( + "/workspace", + "/host/dir", + MountAccess::ReadWrite, + MountKind::Directory, + )]); assert!( - !mounts_openvmm_arguments(std::slice::from_ref(&default)) + !boot_shares(&plan) .unwrap() + .arguments .contains(&"--mount-owner".to_owned()) ); let caller = SandboxMount::parse( @@ -198,22 +230,23 @@ fn mount_owner_defaults_to_vmm_and_forwards_caller() { MountKind::Directory, ) .unwrap(); + deny(&mut plan, 0, &["/host/dir/secrets"]); + let vmm = boot_shares(&plan).unwrap(); + plan.owner = Owner::Caller; + let boot = boot_shares(&plan).unwrap(); assert_eq!( - mounts_openvmm_arguments(std::slice::from_ref(&caller)).unwrap(), + boot.arguments, [ - "--mount", - "/workspace,host-dir,rw", - "--mount-deny", - "secrets", - "--mount-owner", - "caller", + "--mount".to_owned(), + format!("/workspace,{},rw", host_text("/host/dir")), + "--mount-deny".to_owned(), + host_text("/host/dir/secrets"), + "--mount-owner".to_owned(), + "caller".to_owned(), ] ); assert_eq!(caller.absolute().unwrap().owner(), MountOwner::Caller); - assert_eq!( - caller.command_line_fragment(), - default.command_line_fragment() - ); + assert_eq!(boot.fragment, vmm.fragment); assert_eq!( message(MountOwner::from_name("root")), "unsupported sandbox mount owner 'root'; choose vmm or caller" @@ -231,19 +264,29 @@ fn mount_access_policy_is_validated_and_forwarded() { policy(&["mcp-logs"], &["mcp-logs/payloads"], &["agent", "cache"]), ) .unwrap(); + let mut plan = exported(&[( + "/tmp/mcp-runtime", + "/host/dir", + MountAccess::ReadWrite, + MountKind::Directory, + )]); + plan.children[0].write = vec![host("/host/dir/agent"), host("/host/dir/cache")]; + deny(&mut plan, 0, &["/host/dir/mcp-logs"]); + plan.children[0].allowed = vec![host("/host/dir/mcp-logs/payloads")]; + let boot = boot_shares(&plan).unwrap(); assert_eq!( - mounts_openvmm_arguments(std::slice::from_ref(&mount)).unwrap(), + boot.arguments, [ - "--mount", - "/tmp/mcp-runtime,host-dir,rw", - "--mount-deny", - "mcp-logs", - "--mount-allow", - "mcp-logs/payloads", - "--mount-write", - "agent", - "--mount-write", - "cache", + "--mount".to_owned(), + format!("/tmp/mcp-runtime,{},rw", host_text("/host/dir")), + "--mount-deny".to_owned(), + host_text("/host/dir/mcp-logs"), + "--mount-allow".to_owned(), + host_text("/host/dir/mcp-logs/payloads"), + "--mount-write".to_owned(), + host_text("/host/dir/agent"), + "--mount-write".to_owned(), + host_text("/host/dir/cache"), ] ); let absolute = mount.absolute().unwrap(); @@ -252,7 +295,7 @@ fn mount_access_policy_is_validated_and_forwarded() { // OpenVMM enforces the policy on the host, so the guest's bootstrap tokens are those of the // plain share. assert_eq!( - mount.command_line_fragment(), + boot.fragment, " virtfs_dir=/tmp/mcp-runtime virtfs_tag=microvm virtfs_mode=rw" ); let paths: Vec = (0..129).map(|index| format!("path-{index}")).collect(); @@ -528,3 +571,294 @@ fn aggregate_child_names_pin_their_targets() { " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" ); } + +/// Returns the resolved form of `path`, without Windows' verbatim prefix, as the planner records +/// host paths. +fn resolved(path: &PurePath) -> PathBuf { + let canonical = fs::canonicalize(path.as_path()).unwrap(); + canonical + .to_str() + .and_then(|text| text.strip_prefix(r"\\?\")) + .map_or(canonical.clone(), PathBuf::from) +} + +#[test] +fn shares_are_planned_through_the_export_planner() { + let directory = tempfile::tempdir().unwrap(); + let base = PurePath::native(directory.path().to_str().unwrap()); + for name in ["work/secrets/public", "work/out", "tools"] { + fs::create_dir_all(base.join(name).as_path()).unwrap(); + } + fs::write(base.join("settings.json").as_path(), b"{}").unwrap(); + let share = |value: String, policy: MountPolicy, owner: MountOwner, kind: MountKind| { + SandboxMount::parse(&value, policy, owner, kind).unwrap() + }; + let mounts = |owner: MountOwner| { + [ + share( + format!("/workspace,{},rw", base.join("work")), + policy(&["secrets"], &["secrets/public"], &["out"]), + owner, + MountKind::Directory, + ), + share( + format!("/opt/tools,{}", base.join("tools")), + MountPolicy::default(), + owner, + MountKind::Directory, + ), + share( + format!("/etc/app/settings.json,{}", base.join("settings.json")), + MountPolicy::default(), + owner, + MountKind::File, + ), + ] + }; + assert_eq!(plan_mounts(&[]).unwrap(), None); + let plan = plan_mounts(&mounts(MountOwner::Vmm)).unwrap().unwrap(); + let work = resolved(&base.join("work")); + // The children follow their resolved host paths, and each share maps its root at its target, + // parents first, with its policy paths resolved. + let children: Vec<(PathBuf, bool, bool)> = plan + .children + .iter() + .map(|child| (child.root.clone(), child.file, child.writable)) + .collect(); + assert_eq!( + children, + [ + (resolved(&base.join("settings.json")), true, false), + (resolved(&base.join("tools")), false, false), + (work.clone(), false, true), + ] + ); + let workspace = &plan.children[2]; + assert_eq!(workspace.denied, [work.join("secrets")]); + assert_eq!(workspace.allowed, [work.join("secrets").join("public")]); + assert_eq!(workspace.write, [work.join("out")]); + // Writes are narrowed to `out`, so nothing that the plan names can move. + assert_eq!(workspace.denied_identities, [None]); + let binds: Vec<(usize, &str, &str, bool)> = plan + .binds + .iter() + .map(|bind| { + ( + bind.child, + bind.source.as_str(), + bind.target.as_str(), + bind.read_only, + ) + }) + .collect(); + assert_eq!( + binds, + [ + (2, "", "/workspace", false), + (1, "", "/opt/tools", true), + (0, "", "/etc/app/settings.json", true), + ] + ); + assert_eq!(plan.owner, Owner::Vmm); + plan.verify().unwrap(); + plan.check(ReservedTargets::CONTAINER_ROOT).unwrap(); + + // The guest's shell binds each child from its token, in child order. + let names = [ + aggregate_child_name(0, "/etc/app/settings.json"), + aggregate_child_name(1, "/opt/tools"), + aggregate_child_name(2, "/workspace"), + ]; + let boot = boot_shares(&plan).unwrap(); + assert_eq!( + boot.tokens, + [ + format!("nvx_share={},/etc/app/settings.json,ro,file", names[0]), + format!("nvx_share={},/opt/tools,ro", names[1]), + format!("nvx_share={},/workspace,rw", names[2]), + ] + ); + let text = |path: &Path| path.to_str().unwrap().to_owned(); + assert_eq!( + boot.arguments[boot.arguments.len() - 6..], + [ + "--mount-deny".to_owned(), + text(&work.join("secrets")), + "--mount-allow".to_owned(), + text(&work.join("secrets").join("public")), + "--mount-write".to_owned(), + text(&work.join("out")), + ] + ); + + // The planner refuses what OpenVMM would: here, a writable path that a denied path hides. + let error = plan_mounts(&[share( + format!("/workspace,{},rw", base.join("work")), + policy(&["secrets"], &[], &["secrets/public"]), + MountOwner::Vmm, + MountKind::Directory, + )]) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!(error.to_string().contains("which hides it"), "{error}"); + + // The caller owner, on a Linux host, becomes the export's. + #[cfg(unix)] + { + let owned = crate::host::python::fs::stat(directory.path()).unwrap(); + if owned.uid != 0 && owned.gid != 0 { + let plan = plan_mounts(&mounts(MountOwner::Caller)).unwrap().unwrap(); + assert_eq!(plan.owner, Owner::Caller); + assert_eq!( + boot_shares(&plan).unwrap().arguments[boot.arguments.len()..], + ["--mount-owner", "caller"] + ); + } + } +} + +#[test] +fn planned_shares_keep_their_owner_and_kind() { + let directory = tempfile::tempdir().unwrap(); + let base = PurePath::native(directory.path().to_str().unwrap()); + fs::create_dir_all(base.join("work").as_path()).unwrap(); + fs::write(base.join("settings.json").as_path(), b"{}").unwrap(); + let share = |target: &str, name: &str, owner: MountOwner, kind: MountKind| { + SandboxMount::new( + target, + base.join(name).as_str(), + MountAccess::ReadOnly, + MountPolicy::default(), + owner, + kind, + ) + .unwrap() + }; + // One owner performs the operations of every share. + let error = plan_mounts(&[ + share("/workspace", "work", MountOwner::Vmm, MountKind::Directory), + share( + "/etc/app/settings.json", + "settings.json", + MountOwner::Caller, + MountKind::File, + ), + ]) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!( + error.to_string().contains("the same --mount-owner"), + "{error}" + ); + // A share keeps the kind with which it was requested, whatever the planner finds at its host + // path later. + for (name, kind, expected) in [ + ("work", MountKind::File, "is not a regular file"), + ( + "settings.json", + MountKind::Directory, + "is not a plain directory", + ), + ] { + let error = + plan_mounts_with(&[share("/data", name, MountOwner::Vmm, kind)], &cwd).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!(error.to_string().contains(expected), "{error}"); + } + // A host path that leads into another share's directory leaves the share no root of its own. + fs::create_dir_all(base.join("work/nested").as_path()).unwrap(); + let error = plan_mounts_with( + &[ + share("/workspace", "work", MountOwner::Vmm, MountKind::Directory), + share( + "/nested", + "work/nested", + MountOwner::Vmm, + MountKind::Directory, + ), + ], + &cwd, + ) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!( + error + .to_string() + .contains("lies inside the host directory of another share"), + "{error}" + ); +} + +#[test] +fn boot_tokens_refuse_what_the_shell_cannot_bind() { + let plan = exported(&[( + "/workspace", + "/host/work", + MountAccess::ReadWrite, + MountKind::Directory, + )]); + // A path below a share's root. + let mut below = plan.clone(); + below.binds[0].source = "sub".to_owned(); + assert_eq!( + message(boot_shares(&below)), + format!( + "the boot tokens of a share cannot map {}, a path below the share's root {}", + host("/host/work").join("sub").display(), + host_text("/host/work") + ) + ); + // One child at two targets. + let mut twice = plan.clone(); + twice.binds.push(Bind { + target: "/workspace2".to_owned(), + ..twice.binds[0].clone() + }); + assert_eq!( + message(boot_shares(&twice)), + format!( + "the boot tokens of a share map {} at one target, not 2", + host_text("/host/work") + ) + ); + // A bind of no child. + let mut unknown = plan.clone(); + unknown.binds.push(Bind { + child: 1, + target: "/other".to_owned(), + ..unknown.binds[0].clone() + }); + assert_eq!( + message(boot_shares(&unknown)), + "the boot tokens of a share cannot map a path of an unknown host directory" + ); + // A read-only bind of a directory whose writes OpenVMM narrows to a path inside it, which the + // tokens would mount read-write, and a read-write bind of a read-only directory. + let mut narrowed = plan.clone(); + narrowed.children[0].write = vec![host("/host/work/out")]; + narrowed.binds[0].read_only = true; + assert_eq!( + message(boot_shares(&narrowed)), + format!( + "the boot tokens of a share cannot map {} read-only while OpenVMM exports it \ + read-write", + host_text("/host/work") + ) + ); + assert!(narrowed.check(ReservedTargets::CONTAINER_ROOT).is_err()); + let mut read_only = plan.clone(); + read_only.children[0].writable = false; + assert_eq!( + message(boot_shares(&read_only)), + format!( + "the boot tokens of a share cannot map {} read-write while OpenVMM exports it \ + read-only", + host_text("/host/work") + ) + ); + assert!(read_only.check(ReservedTargets::CONTAINER_ROOT).is_err()); + // A target that a token cannot carry. + let mut comma = plan; + comma.binds[0].target = "/work,space".to_owned(); + assert!(message(boot_shares(&comma)).contains("invalid sandbox mount target")); +} diff --git a/nvx-rs/src/host/sandbox/options/tests.rs b/nvx-rs/src/host/sandbox/options/tests.rs index d2187c334..ed26a5a34 100644 --- a/nvx-rs/src/host/sandbox/options/tests.rs +++ b/nvx-rs/src/host/sandbox/options/tests.rs @@ -9,7 +9,7 @@ use super::*; use crate::ErrorKind; use crate::host::python::fs::resolve; use crate::host::sandbox::layer::LayerRole; -use crate::host::sandbox::mount::{aggregate_child_name, mounts_openvmm_arguments}; +use crate::host::sandbox::mount::{aggregate_child_name, plan_mounts, validate_mounts}; use crate::host::sandbox::{SandboxLayer, SandboxSettings}; fn message(result: Result) -> String { @@ -575,12 +575,12 @@ impl Shares { "11111111-1111-1111-1111-111111111111", )], &self.path("scratch.ext4"), - SandboxSettings { - mounts: sandbox_mounts(&mounts, &policy_paths, owner)?, - ..SandboxSettings::default() - }, + SandboxSettings::default(), )?; + let mounts = sandbox_mounts(&mounts, &policy_paths, owner)?; + validate_mounts(&mounts)?; launch.validated()?; + let launch = launch.with_export(plan_mounts(&mounts)?)?; validate_sandbox_systemd_policy(&launch)?; Ok(( launch.plan().openvmm_arguments()?, @@ -602,6 +602,7 @@ fn values(arguments: &[String], option: &str) -> Vec { fn sandbox_command_forwards_mount_to_openvmm() { let shares = Shares::new(); let share = shares.directory("share"); + let secrets = shares.directory("share/secrets"); let (arguments, _) = shares .run( &[format!("/workspace,{share},rw")], @@ -613,8 +614,23 @@ fn sandbox_command_forwards_mount_to_openvmm() { values(&arguments, "--mount"), [format!("/workspace,{share},rw")] ); - assert_eq!(values(&arguments, "--mount-deny"), ["secrets"]); + // The plan resolves each policy path, which must exist. + assert_eq!(values(&arguments, "--mount-deny"), [secrets]); assert!(!arguments.contains(&"--mount-owner".to_owned())); + fs::remove_dir(shares.path("share/secrets")).unwrap(); + let error = shares + .run( + &[format!("/workspace,{share},rw")], + &[(PolicyKind::Denied, Some(0), "secrets")], + None, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("does not exist, so it cannot be hidden"), + "{error}" + ); } #[test] @@ -663,7 +679,10 @@ fn sandbox_command_shares_host_paths_with_commas() { ) .unwrap(); assert_eq!(values(&arguments, "--mount"), [format!("/work,{alpha},rw")]); - assert_eq!(values(&arguments, "--mount-deny"), ["secret"]); + assert_eq!( + values(&arguments, "--mount-deny"), + [shares.path("alpha,beta/secret")] + ); let (arguments, _) = shares .run( &[ @@ -677,9 +696,10 @@ fn sandbox_command_shares_host_paths_with_commas() { None, ) .unwrap(); - let (work, report) = ( - aggregate_child_name(0, "/work"), - aggregate_child_name(1, "/reports"), + // The children follow their resolved host paths. + let (report, work) = ( + aggregate_child_name(0, "/reports"), + aggregate_child_name(1, "/work"), ); let pairs: Vec<(String, String)> = arguments .windows(2) @@ -689,15 +709,15 @@ fn sandbox_command_shares_host_paths_with_commas() { assert_eq!( pairs, [ - ("--mount-child".to_owned(), format!("{work},{alpha},rw")), ("--mount-child".to_owned(), format!("{report},{reports},ro")), + ("--mount-child".to_owned(), format!("{work},{alpha},rw")), ( "--mount-deny".to_owned(), - shares.root.join("alpha,beta/secret").as_str().to_owned() + shares.root.join("2026, final/secret").as_str().to_owned() ), ( "--mount-deny".to_owned(), - shares.root.join("2026, final/secret").as_str().to_owned() + shares.root.join("alpha,beta/secret").as_str().to_owned() ), ] ); @@ -739,6 +759,9 @@ fn sandbox_command_attributes_policy_paths_to_the_preceding_mount() { let shares = Shares::new(); let work = shares.directory("work"); let tools = shares.directory("tools"); + for name in ["work/logs/payloads", "work/out", "tools/credentials"] { + shares.directory(name); + } let mounts = [ format!("/workspace,{work},rw"), format!("/opt/hostedtoolcache,{tools},ro"), @@ -854,15 +877,15 @@ fn sandbox_command_hides_a_share_root_behind_its_allowed_paths() { ) .unwrap(); // OpenVMM hides the workspace's root behind its one allowed path. - let (workspace, toolcache) = ( - aggregate_child_name(0, "/workspace"), - aggregate_child_name(1, "/opt/hostedtoolcache"), + let (toolcache, workspace) = ( + aggregate_child_name(0, "/opt/hostedtoolcache"), + aggregate_child_name(1, "/workspace"), ); assert_eq!( values(&arguments, "--mount-child"), [ - format!("{workspace},{work},rw"), - format!("{toolcache},{tools},ro") + format!("{toolcache},{tools},ro"), + format!("{workspace},{work},rw") ] ); assert_eq!(values(&arguments, "--mount-deny"), [work]); @@ -874,6 +897,9 @@ fn sandbox_command_attributes_mount_deny_to_the_preceding_mount() { let shares = Shares::new(); let work = shares.directory("work"); let tools = shares.directory("tools"); + for name in ["work/secrets", "tools/credentials", "tools/logs"] { + shares.directory(name); + } let mounts = [ format!("/workspace,{work},rw"), format!("/opt/hostedtoolcache,{tools},ro"), @@ -884,29 +910,30 @@ fn sandbox_command_attributes_mount_deny_to_the_preceding_mount() { (PolicyKind::Denied, Some(1), "logs"), ]; let (arguments, command_line) = shares.run(&mounts, &policy, None).unwrap(); - let (workspace, toolcache) = ( - aggregate_child_name(0, "/workspace"), - aggregate_child_name(1, "/opt/hostedtoolcache"), + // The children follow their resolved host paths, each with its own denied paths. + let (toolcache, workspace) = ( + aggregate_child_name(0, "/opt/hostedtoolcache"), + aggregate_child_name(1, "/workspace"), ); assert!(!arguments.contains(&"--mount".to_owned())); assert_eq!(values(&arguments, "--mount-aggregate"), ["/run/nvx/shares"]); assert_eq!( values(&arguments, "--mount-child"), [ - format!("{workspace},{work},rw"), - format!("{toolcache},{tools},ro") + format!("{toolcache},{tools},ro"), + format!("{workspace},{work},rw") ] ); assert_eq!( values(&arguments, "--mount-deny"), [ - shares.root.join("work/secrets").as_str(), shares.root.join("tools/credentials").as_str(), shares.root.join("tools/logs").as_str(), + shares.root.join("work/secrets").as_str(), ] ); assert!(command_line.contains(&format!( - "nvx_share={workspace},/workspace,rw nvx_share={toolcache},/opt/hostedtoolcache,ro" + "nvx_share={toolcache},/opt/hostedtoolcache,ro nvx_share={workspace},/workspace,rw" ))); // With one share, a --mount-deny may come first; with several, it must follow the share @@ -971,10 +998,10 @@ fn sandbox_mounts_take_the_kind_of_each_host_path() { &kind_of, ) .unwrap(); + assert_eq!(parsed[0].kind(), MountKind::Directory); assert_eq!(parsed[1].kind(), MountKind::File); assert_eq!(parsed[0].owner(), MountOwner::Caller); - let arguments = mounts_openvmm_arguments(&parsed).unwrap(); - assert!(arguments.contains(&format!("{},b.json,rw,file", aggregate_child_name(1, "/b")))); + assert_eq!(parsed[1].owner(), MountOwner::Caller); } #[test] diff --git a/nvx-rs/src/host/sandbox/plan.rs b/nvx-rs/src/host/sandbox/plan.rs index 08716fdb4..42446ea3f 100644 --- a/nvx-rs/src/host/sandbox/plan.rs +++ b/nvx-rs/src/host/sandbox/plan.rs @@ -1,6 +1,6 @@ //! The typed plan of a cold layered launch: its block devices, which image over which upper, its -//! launch settings, and its network, from which come OpenVMM's arguments and the kernel command -//! line. +//! launch settings, its live shares, and its network, from which come OpenVMM's arguments and the +//! kernel command line. //! //! A [`SandboxPlan`] holds: //! @@ -10,7 +10,10 @@ //! - an upper, a [`SandboxUpper`]: a whole-device ext4 scratch image on the `scratch` block //! device, or RAM, with no scratch block device; //! - the launch settings, [`SandboxSettings`]: the entrypoint and its arguments, the hostname, the -//! workload identity, the resource limits, and the live shares; +//! workload identity, and the resource limits; +//! - the export plan of the live shares, an [`ExportPlan`] that [`filesystem::plan`](crate::host::filesystem::plan) made for the +//! container root, if the sandbox has shares: the children of OpenVMM's aggregate export, the +//! ordered binds of the guest, and the identities that each start checks again; //! - optionally, a network, [`SandboxNetwork`]: the guest's address and the [`NetworkPolicy`] //! that OpenVMM enforces there, which one lowering, [`network::openvmm_arguments`], renders for //! every caller. A proxy-only policy reports the URL of its proxy as the guest sees it, @@ -22,34 +25,38 @@ //! scratch block device for a RAM upper, and the network device, and //! [`kernel_command_line`](SandboxPlan::kernel_command_line) describes the root with the boot //! tokens of [`protocol::root`](crate::protocol::root), within the budget that OpenVMM leaves the -//! profile. Its arguments name the block devices by their paths; a launch renders them through a -//! [`PreparedLaunch`](super::PreparedLaunch), which names the files that the checks of the images -//! opened instead. +//! profile. Both render the shares as the guest's shell reads them at boot, as +//! [`mount::boot_shares`] does. The plan's arguments name the block devices by their paths; a +//! launch renders them through a [`PreparedLaunch`](super::PreparedLaunch), which names the files +//! that the checks of the images opened instead. //! //! A plan is persistable. Its serde form, of version [`PLAN_VERSION`], requires every field and -//! refuses unknown ones and other versions, and reading it checks the plan as [`SandboxPlan::new`] -//! and [`SandboxNetwork::new`] do: +//! refuses unknown ones and other versions, and reading it checks the plan as +//! [`SandboxPlan::new`], [`SandboxPlan::with_export`], and [`SandboxNetwork::new`] do: //! //! ```json -//! {"version": 2, +//! {"version": 3, //! "distro": {"path": "/images/distro.erofs", "format": "erofs", //! "identity": "11111111-1111-1111-1111-111111111111"}, //! "upper": {"kind": "scratch", "path": "/sandboxes/one/scratch.ext4"}, //! "settings": {"entrypoint": "/bin/sh", "args": [], "hostname": "nvx-sandbox", //! "workloadIdentity": {"uid": 65534, "gid": 65534}, -//! "memoryMax": "268435456", "pidsMax": null, "mounts": [SHARE]}, +//! "memoryMax": "268435456", "pidsMax": null}, +//! "export": EXPORT, //! "network": NETWORK} //! ``` //! //! The `format` of the image is `erofs` or `gpt`, its `identity` a UUID in its canonical text or //! `null`, and a RAM upper is `{"kind": "ram"}`. The limits are decimal strings or `null`, since a -//! memory limit may exceed the integers that a JSON number holds exactly. Each `SHARE` keeps the -//! fields of a [`SandboxMount`]: +//! memory limit may exceed the integers that a JSON number holds exactly. `EXPORT` is `null` +//! without shares, and otherwise the serde form of an [`ExportPlan`]: //! //! ```json -//! {"guestTarget": "/workspace", "hostPath": "/home/user/work", "access": "rw", -//! "policy": {"denied": ["secrets"], "allowed": [], "writable": []}, -//! "owner": "vmm", "kind": "directory"} +//! {"children": [{"root": "/home/user/work", "writable": true, +//! "denied": ["/home/user/work/secrets"], +//! "deniedIdentities": [{"device": 2049, "index": 1835013}], +//! "allowed": [], "write": []}], +//! "binds": [{"child": 0, "source": "", "target": "/workspace", "readOnly": false}]} //! ``` //! //! The `NETWORK` is `null` for no network device, or keeps the fields of a [`SandboxNetwork`] and @@ -78,26 +85,28 @@ //! "ingress": "deny", "hostLoopback": "deny", "hostLoopbackForwards": [], //! "proxy": {"port": 3128, "scheme": "http"}} //! ``` +//! +//! A plan of another version is not read: versions 1 and 2 held the shares as they were +//! requested, which the plan of each launch now resolves. mod form; use std::fmt; -use std::io; use std::net::Ipv4Addr; use std::path::Path; use serde::{Deserialize, Serialize}; use super::distro::{DistroBinding, DistroFormat}; -use super::mount::{self, SandboxMount}; +use super::mount::{self, BootShares}; use super::{ SANDBOX_COMMAND_LINE_MAX_SIZE, SandboxSettings, refused, reject_container_path, reject_disk_path, require_pure_file, validate_settings, }; use crate::error::Result; +use crate::host::filesystem::{ExportPlan, ReservedTargets}; use crate::host::network::{self, NetworkPolicy}; use crate::host::python::builtins::{repr, split, strip}; -use crate::host::python::fs; use crate::host::python::path::{Flavor, PurePath}; use crate::protocol::root::{ DISTRO_MMIO_BASE, ImageFormat, RootImage, RootLayout, RootUpper, SCRATCH_MMIO_BASE, @@ -105,7 +114,7 @@ use crate::protocol::root::{ use crate::uuid::Uuid; /// The version of the serde form of [`SandboxPlan`] that this build writes and reads. -pub const PLAN_VERSION: u32 = 2; +pub const PLAN_VERSION: u32 = 3; /// The role of a block device of a plan, which fixes its MMIO base and its access. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -332,18 +341,21 @@ pub struct SandboxPlan { upper: SandboxUpper, /// The launch settings. settings: SandboxSettings, + /// The export plan of the live shares, if any. + export: Option, /// The network, or `None` for no network device. network: Option, } impl SandboxPlan { - /// Returns the plan of `distro` over `upper`, with `settings` and no network device, which - /// [`with_network`](Self::with_network) adds. + /// Returns the plan of `distro` over `upper`, with `settings`, without live shares, and with + /// no network device; [`with_export`](Self::with_export) and + /// [`with_network`](Self::with_network) add them. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), checking the settings /// in this order: the entrypoint, the arguments, the hostname, a non-root identity, positive - /// limits, limits that the guest's cgroups can apply, at most [`u64::MAX`] bytes of memory and - /// 4,194,303 processes, and shares whose targets do not overlap and that have one owner. + /// limits, limits that the guest's cgroups can apply, and at most [`u64::MAX`] bytes of memory + /// and 4,194,303 processes. pub fn new( distro: SandboxDistro, upper: SandboxUpper, @@ -354,10 +366,24 @@ impl SandboxPlan { distro, upper, settings, + export: None, network: None, }) } + /// Returns the plan with `export`, the export plan of its live shares, if any, which + /// [`filesystem::plan`](crate::host::filesystem::plan) made for the container root. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for an export plan + /// that [`ExportPlan::check`] refuses for the container root, + /// [`ReservedTargets::CONTAINER_ROOT`]. + pub fn with_export(self, export: Option) -> Result { + if let Some(export) = &export { + export.check(ReservedTargets::CONTAINER_ROOT)?; + } + Ok(Self { export, ..self }) + } + /// Returns the plan with `network`, or with no network device for `None`. #[must_use] pub fn with_network(mut self, network: Option) -> Self { @@ -380,9 +406,9 @@ impl SandboxPlan { &self.settings } - /// Returns the shares. - pub fn mounts(&self) -> &[SandboxMount] { - &self.settings.mounts + /// Returns the export plan of the live shares, if any. + pub fn export(&self) -> Option<&ExportPlan> { + self.export.as_ref() } /// Returns the network, or `None` for no network device. @@ -422,7 +448,8 @@ impl SandboxPlan { blocks } - /// Returns the plan with another image, over the same upper and with the same settings. + /// Returns the plan with another image, over the same upper and with the same settings and + /// shares. pub(crate) fn with_image(&self, image: RootImage) -> Self { Self { distro: SandboxDistro::at(self.distro.path.clone(), image), @@ -433,8 +460,10 @@ impl SandboxPlan { /// Checks the plan against the host: each image of its block devices must be a regular file /// whose path has no comma or semicolon, and whose name, and that of the file to which its /// path resolves, as `provision` records it, do not end in `.vhd`, `.vhdx`, `.vmgs`, or - /// `.iso`, with which OpenVMM would not attach it as a raw image; each share must pass - /// [`SandboxMount::validated`], and their host paths [`mount::validate_mount_host_paths`]. + /// `.iso`, with which OpenVMM would not attach it as a raw image; and the export plan's + /// directories and files must still not overlap, by path or through a hard link or bind + /// mount, and the plan must pass [`ExportPlan::verify`], which checks again the host objects + /// that it recorded. /// /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) for a missing image, as /// `distro EROFS layer not found: PATH`, `distro GPT image not found: PATH`, or `ext4 scratch @@ -462,10 +491,21 @@ impl SandboxPlan { reject_container_path(&resolved, &shown)?; } } - for mount in &self.settings.mounts { - mount.validated()?; + match &self.export { + Some(export) => { + mount::check_distinct_export_roots(export, host)?; + export.verify() + } + None => Ok(()), } - mount::validate_mount_host_paths_with(&self.settings.mounts, host) + } + + /// Returns the shares in the form that the guest's shell reads at boot, which + /// [`mount::boot_shares`] renders from the export plan; nothing without shares. + fn boot_shares(&self) -> Result { + self.export + .as_ref() + .map_or_else(|| Ok(BootShares::default()), mount::boot_shares) } /// Returns the OpenVMM arguments of the plan: the microVM machine, a sandbox block device for @@ -473,26 +513,16 @@ impl SandboxPlan { /// image, the workload identity, the arguments that attach the shares, and those of the /// network, [`SandboxNetwork::openvmm_arguments`]. /// - /// Fails as [`mount::mounts_openvmm_arguments`] and [`SandboxNetwork::openvmm_arguments`] do. + /// Fails as [`mount::boot_shares`] and [`SandboxNetwork::openvmm_arguments`] do. pub fn openvmm_arguments(&self) -> Result> { - self.openvmm_arguments_with(&fs::cwd) - } - - /// Implements [`openvmm_arguments`](Self::openvmm_arguments) with `cwd` as `Path.cwd()`. - pub(crate) fn openvmm_arguments_with( - &self, - cwd: &dyn Fn() -> io::Result, - ) -> Result> { - self.render_arguments(&|_, path| path.as_str().to_owned(), cwd) + self.render_arguments(&|_, path| path.as_str().to_owned()) } /// Renders the OpenVMM arguments of [`openvmm_arguments`](Self::openvmm_arguments), with - /// `block` naming the file of each block device, given its role and its path in the plan, and - /// `cwd` as `Path.cwd()`. + /// `block` naming the file of each block device, given its role and its path in the plan. pub(crate) fn render_arguments( &self, block: &dyn Fn(BlockRole, &PurePath) -> String, - cwd: &dyn Fn() -> io::Result, ) -> Result> { let mut arguments = vec!["--machine".to_owned(), "microvm".to_owned()]; for (role, path) in self.pure_blocks() { @@ -505,10 +535,7 @@ impl SandboxPlan { "--microvm-workload-identity".to_owned(), format!("{uid}:{gid}"), ]); - arguments.extend(mount::mounts_openvmm_arguments_with( - &self.settings.mounts, - cwd, - )?); + arguments.extend(self.boot_shares()?.arguments); if let Some(network) = &self.network { arguments.extend(network.openvmm_arguments()?); } @@ -519,14 +546,15 @@ impl SandboxPlan { /// whitespace, then `nvx_sandbox=1`, the boot tokens of the root's image and upper, /// `nvx_entrypoint=`, `nvx_hostname=`, a `nvx_arg=` token for each argument, /// `nvx_memory_max=` and `nvx_pids_max=` when set, and the `nvx_share=` tokens of an - /// aggregate. + /// aggregate, which [`mount::boot_shares`] renders. /// /// The user's command line may hold no NUL and no `nvx_*` or `virtfs_*` token, which the /// profile and OpenVMM own. The command line, the bootstrap tokens that OpenVMM appends for /// the shares, and a NUL must fit in [`SANDBOX_COMMAND_LINE_MAX_SIZE`] bytes. The guest must /// then read the plan's root from the command line: one that [`RootLayout::parse`] refuses /// fails too, as when the user's command line adds a virtio-mmio device at the scratch base - /// beside a RAM upper. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). + /// beside a RAM upper. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), + /// or as [`mount::boot_shares`] does. pub fn kernel_command_line(&self, user_command_line: &str) -> Result { if user_command_line.contains('\0') { return Err(refused("kernel command line contains an embedded NUL")); @@ -564,19 +592,16 @@ impl SandboxPlan { if let Some(pids_max) = &settings.pids_max { tokens.push(format!("nvx_pids_max={}", pids_max.plus_one())); } - if mount::mounts_use_aggregate(&settings.mounts) { - let shares: Vec<_> = settings - .mounts - .iter() - .map(|mount| (mount.guest_target(), mount.access(), mount.kind())) - .collect(); - tokens.extend(mount::aggregate_share_tokens(&shares)); - } + let BootShares { + tokens: shares, + fragment, + .. + } = self.boot_shares()?; + tokens.extend(shares); tokens.retain(|token| !token.is_empty()); let command_line = tokens.join(" "); // OpenVMM appends the shares' bootstrap tokens after this command line, so they consume // the same x86 budget. - let fragment = mount::mounts_command_line_fragment(&settings.mounts); if command_line.len() + fragment.len() + 1 > SANDBOX_COMMAND_LINE_MAX_SIZE { return Err(refused( "sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer \ diff --git a/nvx-rs/src/host/sandbox/plan/form.rs b/nvx-rs/src/host/sandbox/plan/form.rs index c043717ee..b032400f2 100644 --- a/nvx-rs/src/host/sandbox/plan/form.rs +++ b/nvx-rs/src/host/sandbox/plan/form.rs @@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize}; use super::{PLAN_VERSION, SandboxDistro, SandboxNetwork, SandboxPlan, SandboxUpper}; +use crate::host::filesystem::ExportPlan; use crate::host::network::{ Access, Destination, EgressRule, EgressRules, ForwardProtocol, HostLoopback, LoopbackForward, NetworkPolicy, NetworkProxy, PortSelector, Protocol, ProxyScheme, }; use crate::host::python::builtins::{Int, repr}; use crate::host::sandbox::SandboxSettings; -use crate::host::sandbox::mount::{MountAccess, MountKind, MountOwner, MountPolicy, SandboxMount}; use crate::protocol::root::{ImageFormat, RootImage}; use crate::uuid::Uuid; @@ -26,6 +26,9 @@ pub(super) struct PlanForm { upper: UpperForm, /// The launch settings. settings: SettingsForm, + /// The export plan of the live shares, or `None`, which the form requires as `null`. + #[serde(deserialize_with = "Option::deserialize")] + export: Option, /// The network, or `None`, which the form requires as `null`. #[serde(deserialize_with = "Option::deserialize")] network: Option, @@ -86,8 +89,6 @@ struct SettingsForm { /// The decimal text of the process limit, or `None`, which the form requires as `null`. #[serde(deserialize_with = "Option::deserialize")] pids_max: Option, - /// The live shares, in child order. - mounts: Vec, } /// The serde form of a workload identity. @@ -100,36 +101,6 @@ struct IdentityForm { gid: u32, } -/// The serde form of a [`SandboxMount`]. -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct MountForm { - /// The guest directory or file at which the share appears. - guest_target: String, - /// The host directory or file. - host_path: String, - /// The access mode: `ro` or `rw`. - access: String, - /// The access-policy paths. - policy: PolicyForm, - /// The identity of the share's file operations: `vmm` or `caller`. - owner: String, - /// What the share binds: `directory` or `file`. - kind: String, -} - -/// The serde form of a [`MountPolicy`]. -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct PolicyForm { - /// The denied paths. - denied: Vec, - /// The allowed paths. - allowed: Vec, - /// The writable paths. - writable: Vec, -} - /// The serde form of a [`SandboxNetwork`] and its [`NetworkPolicy`]. #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -273,6 +244,7 @@ impl From for PlanForm { distro, upper, settings, + export, network, } = plan; let RootImage { format, identity } = distro.image; @@ -293,6 +265,7 @@ impl From for PlanForm { None => UpperForm::Ram {}, }, settings: SettingsForm::from(settings), + export, network: network.map(NetworkForm::from), } } @@ -302,8 +275,8 @@ impl TryFrom for SandboxPlan { type Error = String; /// Reads a plan, which must be of [`PLAN_VERSION`], hold an identity in its canonical text and - /// limits in decimal text, and pass the checks of [`SandboxMount::new`], - /// [`SandboxPlan::new`], and [`SandboxNetwork::new`]. + /// limits in decimal text, and pass the checks of [`SandboxPlan::new`], + /// [`SandboxPlan::with_export`], and [`SandboxNetwork::new`]. fn try_from(form: PlanForm) -> Result { if form.version != PLAN_VERSION { return Err(format!( @@ -337,7 +310,9 @@ impl TryFrom for SandboxPlan { UpperForm::Ram {} => SandboxUpper::RAM, }; let settings = SandboxSettings::try_from(form.settings)?; - let plan = Self::new(distro, upper, settings).map_err(|error| error.to_string())?; + let plan = Self::new(distro, upper, settings) + .and_then(|plan| plan.with_export(form.export)) + .map_err(|error| error.to_string())?; let network = form.network.map(SandboxNetwork::try_from).transpose()?; Ok(plan.with_network(network)) } @@ -524,7 +499,6 @@ impl From for SettingsForm { workload_identity: IdentityForm { uid, gid }, memory_max: settings.memory_max.map(|limit| limit.to_string()), pids_max: settings.pids_max.map(|limit| limit.to_string()), - mounts: settings.mounts.iter().map(MountForm::from).collect(), } } } @@ -533,7 +507,7 @@ impl TryFrom for SandboxSettings { type Error = String; /// Reads settings whose limits are positive integers in decimal text, without a sign or - /// leading zeros, and whose shares pass the checks of [`SandboxMount::new`]. + /// leading zeros. fn try_from(form: SettingsForm) -> Result { let limit = |name: &str, text: Option| { text.map(|text| { @@ -561,61 +535,6 @@ impl TryFrom for SandboxSettings { workload_identity: (uid, gid), memory_max: limit("memoryMax", form.memory_max)?, pids_max: limit("pidsMax", form.pids_max)?, - mounts: form - .mounts - .into_iter() - .map(SandboxMount::try_from) - .collect::>()?, }) } } - -impl From<&SandboxMount> for MountForm { - fn from(mount: &SandboxMount) -> Self { - let MountPolicy { - denied, - allowed, - writable, - } = mount.policy().clone(); - Self { - guest_target: mount.guest_target().to_owned(), - // A share's host path is text, which `pathlib` normalized. - host_path: mount.host_path().to_string_lossy().into_owned(), - access: mount.access().as_str().to_owned(), - policy: PolicyForm { - denied, - allowed, - writable, - }, - owner: mount.owner().as_str().to_owned(), - kind: mount.kind().as_str().to_owned(), - } - } -} - -impl TryFrom for SandboxMount { - type Error = String; - - /// Reads a share as [`SandboxMount::new`] checks it. - fn try_from(form: MountForm) -> Result { - let message = |error: crate::Error| error.to_string(); - let PolicyForm { - denied, - allowed, - writable, - } = form.policy; - Self::new( - &form.guest_target, - &form.host_path, - MountAccess::from_name(&form.access).map_err(message)?, - MountPolicy { - denied, - allowed, - writable, - }, - MountOwner::from_name(&form.owner).map_err(message)?, - MountKind::from_name(&form.kind).map_err(message)?, - ) - .map_err(message) - } -} diff --git a/nvx-rs/src/host/sandbox/plan/tests.rs b/nvx-rs/src/host/sandbox/plan/tests.rs index 7758c9fb8..7b4d6eb24 100644 --- a/nvx-rs/src/host/sandbox/plan/tests.rs +++ b/nvx-rs/src/host/sandbox/plan/tests.rs @@ -8,16 +8,16 @@ use serde_json::{Value, json}; use super::*; use crate::ErrorKind; +use crate::host::filesystem::{Bind, FileIdentity}; use crate::host::network::{ Access, Destination, EgressRule, ForwardProtocol, HostLoopback, LoopbackForward, NetworkProxy, Protocol, ProxyScheme, }; use crate::host::python::builtins::Int; use crate::host::sandbox::distro::DistroBinding; -use crate::host::sandbox::{ - LayerRole, MountAccess, MountKind, MountOwner, MountPolicy, SandboxLaunch, SandboxLayer, - mount::mounts_command_line_fragment, -}; +use crate::host::sandbox::mount::boot_shares; +use crate::host::sandbox::tests::{deny, exported, host, host_text}; +use crate::host::sandbox::{LayerRole, MountAccess, MountKind, SandboxLaunch, SandboxLayer}; const EROFS_UUID: &str = "11111111-1111-1111-1111-111111111111"; const GPT_GUID: &str = "fedcba98-7654-3210-fedc-ba9876543210"; @@ -46,24 +46,63 @@ fn uppers() -> [SandboxUpper; 2] { } fn plan(image: RootImage, upper: SandboxUpper, settings: SandboxSettings) -> SandboxPlan { + exporting(image, upper, settings, None) +} + +fn exporting( + image: RootImage, + upper: SandboxUpper, + settings: SandboxSettings, + export: Option, +) -> SandboxPlan { SandboxPlan::new( SandboxDistro::new("images/distro.img", image), upper, settings, ) + .and_then(|plan| plan.with_export(export)) .unwrap() } -fn share(target: &str, host_path: &str, access: MountAccess) -> SandboxMount { - SandboxMount::new( - target, - host_path, - access, - MountPolicy::default(), - MountOwner::Vmm, +/// Returns the export plan of a read-write directory at `/workspace`, and of a read-only one at +/// `/srv/cache` unless `one`. +fn shares(one: bool) -> ExportPlan { + let workspace = ( + "/workspace", + "/host/work", + MountAccess::ReadWrite, MountKind::Directory, - ) - .unwrap() + ); + let cache = ( + "/srv/cache", + "/host/cache", + MountAccess::ReadOnly, + MountKind::Directory, + ); + if one { + exported(&[workspace]) + } else { + exported(&[workspace, cache]) + } +} + +/// Returns a read-write bind of `/host/work/out`, inside the first child of [`shares`], at +/// `/workspace/out`, which pins `identity`. +fn nested(identity: Option) -> Bind { + Bind { + child: 0, + source: "out".to_owned(), + target: "/workspace/out".to_owned(), + read_only: false, + identity, + } +} + +/// Returns the bootstrap tokens that OpenVMM appends for the shares of `plan`. +fn fragment(plan: &SandboxPlan) -> String { + plan.export() + .map(|export| boot_shares(export).unwrap().fragment) + .unwrap_or_default() } #[test] @@ -75,7 +114,6 @@ fn every_format_and_upper_plans_and_renders() { workload_identity: (1000, 1001), memory_max: Some(Int::from(268_435_456_u32)), pids_max: Some(Int::from(64_u32)), - mounts: Vec::new(), }; let distro = "nvx_layer=distro,0xd0003000"; let cases = [ @@ -150,15 +188,11 @@ fn every_format_and_upper_plans_and_renders() { fn the_guest_reads_the_root_of_every_plan_from_its_command_line() { let settings = SandboxSettings { args: vec!["-c".to_owned(), "true".to_owned()], - mounts: vec![ - share("/workspace", "work", MountAccess::ReadWrite), - share("/srv/cache", "cache", MountAccess::ReadOnly), - ], ..SandboxSettings::default() }; for image in images() { for upper in uppers() { - let plan = plan(image, upper.clone(), settings.clone()); + let plan = exporting(image, upper.clone(), settings.clone(), Some(shares(false))); let command_line = plan.kernel_command_line("console=hvc0 quiet").unwrap(); assert_eq!( RootLayout::parse(command_line.as_bytes()), @@ -172,7 +206,7 @@ fn the_guest_reads_the_root_of_every_plan_from_its_command_line() { if upper.scratch_path().is_some() { effective.push_str(" virtio_mmio.device=0x1000@0xd0006000:11"); } - effective.push_str(&mounts_command_line_fragment(plan.mounts())); + effective.push_str(&fragment(&plan)); assert_eq!( RootLayout::parse(effective.as_bytes()), Ok(Some(plan.layout())), @@ -226,23 +260,12 @@ fn the_longest_boot_tokens_fit_the_command_line_budget_to_the_byte() { // of the budget, which every other plan keeps the rest of. let longest = plan(images()[0], uppers()[0].clone(), SandboxSettings::default()); let longest_base = longest.kernel_command_line("").unwrap().len(); - for mounts in [ - Vec::new(), - vec![share("/workspace", "work", MountAccess::ReadWrite)], - vec![ - share("/workspace", "work", MountAccess::ReadWrite), - share("/srv/cache", "cache", MountAccess::ReadOnly), - ], - ] { - let settings = SandboxSettings { - mounts, - ..SandboxSettings::default() - }; + for export in [None, Some(shares(true)), Some(shares(false))] { for image in images() { for upper in uppers() { - let plan = plan(image, upper, settings.clone()); + let plan = exporting(image, upper, SandboxSettings::default(), export.clone()); let base = plan.kernel_command_line("").unwrap(); - let fragment = mounts_command_line_fragment(plan.mounts()); + let fragment = fragment(&plan); // The user's command line, a space, the plan's tokens, OpenVMM's share tokens, // and a NUL fill the budget exactly. let fitting = @@ -259,7 +282,7 @@ fn the_longest_boot_tokens_fit_the_command_line_budget_to_the_byte() { assert!( message(plan.kernel_command_line(&format!("{fitting}x"))).contains("1024-byte") ); - if plan.mounts().is_empty() { + if plan.export().is_none() { assert!(base.len() <= longest_base, "{base}"); } } @@ -316,22 +339,358 @@ fn plans_check_their_settings_as_launches_do() { |settings| settings.memory_max = Some(Int::zero()), "--memory-max must be positive", ), + ] { + let error = checked(change).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert_eq!(error.to_string(), expected); + } + checked(|_| {}).unwrap(); +} + +#[test] +fn plans_check_their_export_plan_for_the_container_root() { + let checked = |change: fn(&mut ExportPlan)| { + let mut export = shares(false); + change(&mut export); + SandboxPlan::new( + SandboxDistro::new("d.img", RootImage::gpt(None)), + SandboxUpper::RAM, + SandboxSettings::default(), + ) + .and_then(|plan| plan.with_export(Some(export))) + }; + checked(|_| {}).unwrap(); + // A hidden root shows the paths that it shows again. + checked(|export| { + deny(export, 0, &["/host/work"]); + export.children[0].allowed = vec![host("/host/work/public")]; + }) + .unwrap(); + // A child that the guest may write all of pins the denied and mapped objects inside it. + checked(|export| { + deny(export, 0, &["/host/work/secrets"]); + export.binds.push(nested(Some(FileIdentity { + device: 1, + index: 9, + }))); + }) + .unwrap(); + // Writes narrowed to `out` move only what lies strictly inside it, not `out` itself. + checked(|export| { + export.children[0].write = vec![host("/host/work/out")]; + deny(export, 0, &["/host/work/secrets", "/host/work/out/secret"]); + export.binds.push(nested(None)); + }) + .unwrap(); + // A child whose root is mapped read-only writes the paths that it maps read-write. + checked(|export| { + export.binds[0].read_only = true; + export.children[0].write = vec![host("/host/work/out")]; + export.binds.push(nested(None)); + }) + .unwrap(); + // A source holds the host's names joined with `/`: a Windows host reads a backslash in it as a + // separator, which another host keeps in the name. + let backslash = checked(|export| { + export.binds.push(Bind { + source: r"src\host".to_owned(), + target: r"/workspace/src\host".to_owned(), + ..nested(Some(FileIdentity { + device: 1, + index: 9, + })) + }); + }); + if cfg!(windows) { + let error = backslash.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!( + error.to_string().contains( + r#"it maps the path "src\\host" of child 0, which the host reads as other names"# + ), + "{error}" + ); + } else { + backslash.unwrap(); + } + for (change, expected) in [ + ( + (|export: &mut ExportPlan| export.binds[0].target = "/etc".to_owned()) + as fn(&mut ExportPlan), + "its guest target /etc would cover the guest's /etc", + ), + ( + |export| export.binds[0].target = "/run".to_owned(), + "its guest target /run would cover the guest's /run", + ), + ( + |export| export.binds[1].target = "/proc/self".to_owned(), + "its guest target /proc/self would cover the guest's /proc", + ), + ( + |export| export.binds[0].target = "workspace".to_owned(), + "its guest target \"workspace\" is not absolute", + ), + ( + |export| export.binds[1].target = "/workspace/cache".to_owned(), + "it maps /workspace/cache inside /workspace, where it would hide part of that path", + ), + ( + |export| export.binds.swap(0, 1), + "it maps /workspace after /srv/cache, out of order or twice", + ), + ( + |export| export.binds[1].child = 7, + "it maps no path at the root of child 1", + ), + ( + |export| export.binds[0].source = "sub".to_owned(), + "it maps no path at the root of child 0", + ), + ( + |export| export.children[0].denied = vec![host("/host/work/secrets")], + "child 0 has 1 denied paths but 0 identities", + ), + ( + |export| deny(export, 0, &["/host/cache/secrets"]), + "child 0 has the policy path", + ), ( - |settings| { - settings.mounts = vec![ - share("/workspace", "a", MountAccess::ReadWrite), - share("/workspace/cache", "b", MountAccess::ReadOnly), + |export| export.children[1].write = vec![host("/host/cache/out")], + "child 1 is read-only but has writable paths", + ), + ( + |export| export.binds[1].read_only = false, + "child 1 is read-only but maps a path read-write", + ), + ( + // A read-only bind of a directory whose writes OpenVMM narrows to a path inside it. + |export| { + export.children[0].write = vec![host("/host/work/out")]; + export.binds[0].read_only = true; + }, + "child 0 is writable but maps no path read-write", + ), + ( + |export| { + export.children[0].denied = (0..129) + .map(|index| host(&format!("/host/work/secret-{index}"))) + .collect(); + export.children[0].denied_identities = vec![None; 129]; + }, + "child 0: OpenVMM can hide at most 128 paths in one exported directory", + ), + ( + |export| { + export.children[0].write = vec![host(&format!("/host/work/{}", "x".repeat(4097)))]; + }, + "child 0: OpenVMM cannot make writable", + ), + ( + |export| { + export.children[0].write = (0..5) + .map(|index| host(&format!("/host/work/{index}{}", "x".repeat(3999)))) + .collect(); + }, + "exceed its 16384-byte limit", + ), + ( + |export| { + export.binds.insert( + 0, + Bind { + child: 0, + source: "sub\0file".to_owned(), + target: "/data".to_owned(), + read_only: false, + identity: None, + }, + ); + }, + "which contains a NUL character", + ), + ( + |export| { + export.binds.insert( + 0, + Bind { + child: 0, + source: "x".repeat(4090), + target: "/data".to_owned(), + read_only: false, + identity: None, + }, + ); + }, + "which is too long for a guest path", + ), + ( + |export| export.children[1].root = host("/host/work/cache"), + "overlaps child 0", + ), + ( + |export| deny(export, 0, &["/host/work/secrets", "/host/work/secrets"]), + "lists the hidden path", + ), + ( + |export| { + deny( + export, + 0, + &["/host/work/secrets", "/host/work/secrets/keys"], + ) + }, + "which it hides already", + ), + ( + |export| export.children[0].allowed = vec![host("/host/work/public")], + "again, but hides no path that contains it", + ), + ( + |export| { + deny(export, 0, &["/host/work/secrets"]); + export.children[0].allowed = vec![host("/host/work/secrets")]; + }, + "it both hides and shows", + ), + ( + |export| { + deny(export, 0, &["/host/work/secrets"]); + export.children[0].allowed = vec![ + host("/host/work/secrets/public"), + host("/host/work/secrets/public/docs"), ]; }, - "sandbox mount targets /workspace and /workspace/cache overlap; a share cannot hide \ - another", + "which it shows already", + ), + ( + // OpenVMM makes no hidden path writable. + |export| { + deny(export, 0, &["/host/work/secrets"]); + export.children[0].write = vec![host("/host/work/secrets/public")]; + }, + "writable, but", + ), + ( + |export| export.children[0].write = vec![host("/host/work")], + "writable through a writable path", + ), + ( + |export| { + export.children[0].write = + vec![host("/host/work/out"), host("/host/work/out/logs")]; + }, + "which it makes writable already", + ), + ( + |export| deny(export, 0, &["/host/work"]), + "/workspace, but child 0 hides it", + ), + ( + // A path that a hidden directory holds is hidden too, unless it shows a path again. + |export| { + deny(export, 0, &["/host/work"]); + export.children[0].allowed = vec![host("/host/work/public")]; + export.binds.push(Bind { + child: 0, + source: "private".to_owned(), + target: "/workspace/private".to_owned(), + read_only: false, + identity: None, + }); + }, + "/workspace/private, but child 0 hides it", + ), + ( + |export| export.children.clear(), + "it exports 0 directories and files", + ), + ( + // A read-only child whose denied path no process argument could carry. + |export| deny(export, 1, &["/host/cache/secret\0suffix"]), + r#"secret\0suffix", which contains a NUL character"#, + ), + ( + |export| export.children[1].root = host("/host/ca\0che"), + "which contains a NUL character", + ), + ( + // A path that lies inside a child by its text, but not on the host. + |export| deny(export, 0, &["/host/work/../cache/secret"]), + "which has a `..` component", + ), + ( + |export| export.children[1].root = host("/host/work/../cache"), + "which has a `..` component", + ), + ( + // The guest would write `other`, which no bind maps read-write, but not `out`. + |export| { + export.binds[0].read_only = true; + export.children[0].write = vec![host("/host/work/other")]; + export.binds.push(nested(None)); + }, + "child 0 lets the guest write other paths than those that it maps read-write", + ), + ( + // Without write paths, the guest would write all of the child. + |export| { + export.binds[0].read_only = true; + export.binds.push(nested(None)); + }, + "child 0 lets the guest write other paths than those that it maps read-write", + ), + ( + |export| { + export.children[0].write = vec![host("/host/work/other")]; + export.binds.push(nested(None)); + }, + "which it maps read-write, stays read-only", + ), + ( + |export| { + export.children[0].denied = vec![host("/host/work/secrets")]; + export.children[0].denied_identities = vec![None]; + }, + "child 0 does not pin the denied path", + ), + ( + |export| { + export.children[0].write = vec![host("/host/work/out")]; + export.children[0].denied = vec![host("/host/work/out/secret")]; + export.children[0].denied_identities = vec![None]; + }, + "child 0 does not pin the denied path", + ), + ( + |export| export.binds.push(nested(None)), + "it does not pin the object that it maps at /workspace/out, which a workload could \ + move", ), ] { let error = checked(change).unwrap_err(); assert_eq!(error.kind(), ErrorKind::Unsupported); - assert_eq!(error.to_string(), expected); + let message = error.to_string(); + assert!( + message.starts_with("the recorded export plan is invalid: ") + && message.contains(expected), + "{message} lacks {expected}" + ); + } + // On Windows, `plan` resolves paths on local drives, without the verbatim prefix. + if cfg!(windows) { + for change in [ + (|export: &mut ExportPlan| export.children[1].root = r"\\?\C:\host\cache".into()) + as fn(&mut ExportPlan), + |export| export.children[1].root = r"\\server\share\cache".into(), + ] { + let message = checked(change).unwrap_err().to_string(); + assert!( + message.ends_with("which is not a path on a local drive"), + "{message}" + ); + } } - checked(|_| {}).unwrap(); } #[test] @@ -580,19 +939,6 @@ fn launches_plan_the_image_that_the_binding_of_their_distro_layer_records() { /// Returns a plan of every kind of setting, persisted as it records them. fn persisted() -> (SandboxPlan, Value) { - let workspace = SandboxMount::new( - "/workspace", - "work", - MountAccess::ReadWrite, - MountPolicy { - denied: vec!["secrets".to_owned()], - allowed: vec!["secrets/public".to_owned()], - writable: vec!["out".to_owned()], - }, - MountOwner::Vmm, - MountKind::Directory, - ) - .unwrap(); // The largest limits that the guest's cgroups apply, of which the memory limit exceeds the // integers that a JSON number holds exactly. let settings = SandboxSettings { @@ -602,29 +948,35 @@ fn persisted() -> (SandboxPlan, Value) { workload_identity: (1000, 1001), memory_max: Some(Int::parse("18446744073709551615").unwrap()), pids_max: Some(Int::from(4_194_303_u32)), - mounts: vec![ - workspace, - SandboxMount::new( - "/etc/app/settings.json", - "settings.json", - MountAccess::ReadOnly, - MountPolicy::default(), - MountOwner::Vmm, - MountKind::File, - ) - .unwrap(), - ], }; + let mut export = exported(&[ + ( + "/workspace", + "/host/work", + MountAccess::ReadWrite, + MountKind::Directory, + ), + ( + "/etc/app/settings.json", + "/host/settings.json", + MountAccess::ReadOnly, + MountKind::File, + ), + ]); + export.children[0].write = vec![host("/host/work/out")]; + deny(&mut export, 0, &["/host/work/secrets"]); + export.children[0].allowed = vec![host("/host/work/secrets/public")]; let plan = SandboxPlan::new( SandboxDistro::new("/images/distro.img", RootImage::gpt(Some(uuid(GPT_GUID)))), SandboxUpper::RAM, settings, ) + .and_then(|plan| plan.with_export(Some(export))) .unwrap() .with_network(Some(direct_network())); let path = |text: &str| PurePath::native(text).as_str().to_owned(); let form = json!({ - "version": 2, + "version": 3, "distro": {"path": path("/images/distro.img"), "format": "gpt", "identity": GPT_GUID}, "upper": {"kind": "ram"}, "settings": { @@ -634,28 +986,31 @@ fn persisted() -> (SandboxPlan, Value) { "workloadIdentity": {"uid": 1000, "gid": 1001}, "memoryMax": "18446744073709551615", "pidsMax": "4194303", - "mounts": [ + }, + "export": { + "children": [ { - "guestTarget": "/workspace", - "hostPath": "work", - "access": "rw", - "policy": { - "denied": ["secrets"], - "allowed": ["secrets/public"], - "writable": ["out"], - }, - "owner": "vmm", - "kind": "directory", + "root": host_text("/host/work"), + "writable": true, + "denied": [host_text("/host/work/secrets")], + "deniedIdentities": [null], + "allowed": [host_text("/host/work/secrets/public")], + "write": [host_text("/host/work/out")], }, { - "guestTarget": "/etc/app/settings.json", - "hostPath": "settings.json", - "access": "ro", - "policy": {"denied": [], "allowed": [], "writable": []}, - "owner": "vmm", - "kind": "file", + "root": host_text("/host/settings.json"), + "file": true, + "writable": false, + "denied": [], + "deniedIdentities": [], + "allowed": [], + "write": [], }, ], + "binds": [ + {"child": 0, "source": "", "target": "/workspace", "readOnly": false}, + {"child": 1, "source": "", "target": "/etc/app/settings.json", "readOnly": true}, + ], }, "network": direct_network_form(), }); @@ -761,6 +1116,7 @@ fn plans_persist_in_their_versioned_form() { json!({"kind": "scratch", "path": PurePath::native("scratch.ext4").as_str()}) ); assert_eq!(scratch["distro"]["identity"], Value::Null); + assert_eq!(scratch["export"], Value::Null); assert_eq!(scratch["network"], Value::Null); let proxy = serde_json::to_value(proxy_network(Some(ProxyScheme::Https))).unwrap(); assert_eq!( @@ -776,7 +1132,7 @@ fn plans_persist_in_their_versioned_form() { "proxy": {"port": 3128, "scheme": "https"}, }) ); - assert_eq!(PLAN_VERSION, 2); + assert_eq!(PLAN_VERSION, 3); } fn plan_with(image: RootImage, upper: SandboxUpper) -> SandboxPlan { @@ -811,11 +1167,13 @@ fn persisted_plans_that_break_the_form_or_the_checks_are_refused() { "/settings/workloadIdentity/uid", "/settings/memoryMax", "/settings/pidsMax", - "/settings/mounts", - "/settings/mounts/0/guestTarget", - "/settings/mounts/0/policy", - "/settings/mounts/0/policy/writable", - "/settings/mounts/1/kind", + "/export", + "/export/children", + "/export/children/0/root", + "/export/children/0/deniedIdentities", + "/export/binds", + "/export/binds/0/target", + "/export/binds/1/readOnly", "/network", "/network/guestNetwork", "/network/egress", @@ -854,8 +1212,9 @@ fn persisted_plans_that_break_the_form_or_the_checks_are_refused() { "/upper", "/settings", "/settings/workloadIdentity", - "/settings/mounts/0", - "/settings/mounts/0/policy", + "/export", + "/export/children/0", + "/export/binds/0", "/network", "/network/allow", "/network/allow/rules/0", @@ -888,13 +1247,14 @@ fn persisted_plans_that_break_the_form_or_the_checks_are_refused() { &|value| value["distro"]["format"] = json!("ext4"), "unknown variant", ); - for version in [1, 3] { + for version in [1, 2, 4] { refused( &|value| value["version"] = json!(version), &format!("the sandbox plan has version {version}, which this build does not read"), ); } - // The form of version 1, which had no network, is not read. + // The forms of versions 1 and 2 held the shares as requested, and that of version 1 had no + // network: neither is read. refused( &|value| { value["version"] = json!(1); @@ -902,6 +1262,13 @@ fn persisted_plans_that_break_the_form_or_the_checks_are_refused() { }, "missing field `network`", ); + refused( + &|value| { + value["version"] = json!(2); + value["settings"]["mounts"] = json!([]); + }, + "unknown field `mounts`", + ); for identity in ["", "FEDCBA98-7654-3210-FEDC-BA9876543210", "{fedcba98}"] { refused( &|value| value["distro"]["identity"] = json!(identity), @@ -943,24 +1310,20 @@ fn persisted_plans_that_break_the_form_or_the_checks_are_refused() { "sandbox entrypoint must be an absolute path without spaces or quotes", ); refused( - &|value| value["settings"]["mounts"][1]["guestTarget"] = json!("/workspace/settings.json"), - "overlap", - ); - refused( - &|value| value["settings"]["mounts"][0]["guestTarget"] = json!("/etc"), - "sandbox mount target /etc is reserved", + &|value| value["export"]["binds"][1]["target"] = json!("/workspace/settings.json"), + "would hide part of that path", ); refused( - &|value| value["settings"]["mounts"][0]["access"] = json!("ro"), - "--mount-write narrows the writes of a read-write share", + &|value| value["export"]["binds"][0]["target"] = json!("/etc"), + "its guest target /etc would cover the guest's /etc", ); refused( - &|value| value["settings"]["mounts"][0]["owner"] = json!("root"), - "unsupported sandbox mount owner", + &|value| value["export"]["owner"] = json!("root"), + "unknown variant `root`", ); refused( - &|value| value["settings"]["mounts"][1]["kind"] = json!("socket"), - "unsupported sandbox mount kind 'socket'", + &|value| value["export"]["children"] = json!([]), + "the recorded export plan is invalid", ); // The network's values, and what its policy checks. refused( @@ -1044,11 +1407,12 @@ fn persisted_plans_that_break_the_form_or_the_checks_are_refused() { #[test] fn plans_render_and_report_their_network() { - let settings = SandboxSettings { - mounts: vec![share("/workspace", "work", MountAccess::ReadWrite)], - ..SandboxSettings::default() - }; - let without = plan(images()[0], uppers()[0].clone(), settings); + let without = exporting( + images()[0], + uppers()[0].clone(), + SandboxSettings::default(), + Some(shares(true)), + ); let shares = without.openvmm_arguments().unwrap(); assert!(without.network().is_none()); assert_eq!(without.proxy_url(), None); diff --git a/nvx-rs/src/host/sandbox/prepared.rs b/nvx-rs/src/host/sandbox/prepared.rs index 381c59c16..a743f80f9 100644 --- a/nvx-rs/src/host/sandbox/prepared.rs +++ b/nvx-rs/src/host/sandbox/prepared.rs @@ -49,7 +49,6 @@ //! or GID, so the VMM's `--microvm-workload-identity` never encodes root. use std::fs::File; -use std::io; use std::path::Path; use super::distro::CheckedDistro; @@ -61,7 +60,7 @@ use super::scratch::CheckedScratch; #[cfg(windows)] use super::{reject_container_path, reject_disk_path}; use crate::error::Result; -use crate::host::python::fs; +#[cfg(windows)] use crate::host::python::path::PurePath; #[cfg(windows)] use crate::host::seal; @@ -152,17 +151,6 @@ impl PreparedLaunch { distro: CheckedDistro, scratch: Option, user_command_line: &str, - ) -> Result { - Self::new_with(plan, distro, scratch, user_command_line, &fs::cwd) - } - - /// Implements [`new`](Self::new) with `cwd` as `Path.cwd()`. - pub(crate) fn new_with( - plan: SandboxPlan, - distro: CheckedDistro, - scratch: Option, - user_command_line: &str, - cwd: &dyn Fn() -> io::Result, ) -> Result { let planned = plan.distro().path(); if !same_path(distro.path(), planned) { @@ -209,16 +197,13 @@ impl PreparedLaunch { Some(scratch) => Some(checked_path(scratch.file(), scratch.path())?), None => None, }; - let openvmm_arguments = plan.render_arguments( - &|role, _| match (role, &scratch_block) { - (BlockRole::Distro, _) => distro_block.clone(), - (BlockRole::Scratch, Some(block)) => block.clone(), - (BlockRole::Scratch, None) => { - unreachable!("a plan with a scratch block has its checked scratch image") - } - }, - cwd, - )?; + let openvmm_arguments = plan.render_arguments(&|role, _| match (role, &scratch_block) { + (BlockRole::Distro, _) => distro_block.clone(), + (BlockRole::Scratch, Some(block)) => block.clone(), + (BlockRole::Scratch, None) => { + unreachable!("a plan with a scratch block has its checked scratch image") + } + })?; let kernel_command_line = plan.kernel_command_line(user_command_line)?; Ok(Self { plan, diff --git a/nvx-rs/src/host/sandbox/prepared/tests.rs b/nvx-rs/src/host/sandbox/prepared/tests.rs index 512410929..b3b062822 100644 --- a/nvx-rs/src/host/sandbox/prepared/tests.rs +++ b/nvx-rs/src/host/sandbox/prepared/tests.rs @@ -12,6 +12,7 @@ use sha2::{Digest, Sha256}; use super::*; use crate::ErrorKind; +use crate::host::python::path::PurePath; use crate::host::sandbox::distro::{self, DISTRO_POLICY_REVISION, DistroBinding, PolicyVerdict}; use crate::host::sandbox::scratch::{self, ScratchBinding}; use crate::host::sandbox::{ @@ -50,7 +51,7 @@ const WRITE_ONLY: scratch::Access<'static> = scratch::Access::WRITABLE; /// Opens `path` for writing alone. #[cfg(target_os = "linux")] -fn open_write_only(path: &Path) -> io::Result { +fn open_write_only(path: &Path) -> std::io::Result { fs::OpenOptions::new().write(true).open(path) } diff --git a/nvx-rs/src/host/sandbox/tests.rs b/nvx-rs/src/host/sandbox/tests.rs index 499f7ded8..ca1854d9e 100644 --- a/nvx-rs/src/host/sandbox/tests.rs +++ b/nvx-rs/src/host/sandbox/tests.rs @@ -1,10 +1,11 @@ //! The launch-profile tests of `scripts/test_nvx_tools.py`, ported. use std::fs; +use std::path::PathBuf; use super::*; use crate::ErrorKind; -use crate::host::python::fs::cwd; +use crate::host::filesystem::{Bind, ExportChild, FileIdentity, Owner}; use crate::protocol::root::{RootLayout, RootUpper}; const DISTRO: &str = "distro,distro.erofs,11111111-1111-1111-1111-111111111111"; @@ -34,15 +35,87 @@ fn mount(value: &str, denied: &[&str]) -> SandboxMount { .unwrap() } -fn with_mounts(mounts: Vec) -> Result { - SandboxLaunch::new( - vec![distro()], - "scratch.ext4", - SandboxSettings { - mounts, - ..SandboxSettings::default() - }, - ) +fn with_export(export: Option) -> Result { + SandboxLaunch::new(vec![distro()], "scratch.ext4", SandboxSettings::default())? + .with_export(export) +} + +/// Returns `path`, an absolute POSIX path, as an absolute host path: on Windows, on drive `C:`. +pub(super) fn host(path: &str) -> PathBuf { + if cfg!(windows) { + PathBuf::from(format!("C:{}", path.replace('/', "\\"))) + } else { + PathBuf::from(path) + } +} + +/// Returns the text of [`host`] of `path`. +pub(super) fn host_text(path: &str) -> String { + host(path).to_str().unwrap().to_owned() +} + +/// Returns the export plan that maps each of `shares`, a guest target, an absolute POSIX host path +/// that [`host`] places, an access mode, and a kind, at the root of a child of its own, in order, +/// without access-policy paths, as [`mount::plan_mounts`] plans shares. +pub(super) fn exported(shares: &[(&str, &str, MountAccess, MountKind)]) -> ExportPlan { + let children = shares + .iter() + .map(|&(_, root, access, kind)| ExportChild { + root: host(root), + file: kind == MountKind::File, + writable: access == MountAccess::ReadWrite, + denied: Vec::new(), + denied_identities: Vec::new(), + allowed: Vec::new(), + write: Vec::new(), + }) + .collect(); + let mut binds: Vec = shares + .iter() + .enumerate() + .map(|(child, &(target, _, access, _))| Bind { + child, + source: String::new(), + target: target.to_owned(), + read_only: access == MountAccess::ReadOnly, + identity: None, + }) + .collect(); + // Parents bind before children, as the planner orders them. + binds.sort_by(|left, right| { + (left.target.matches('/').count(), &left.target) + .cmp(&(right.target.matches('/').count(), &right.target)) + }); + ExportPlan { + children, + binds, + owner: Owner::Vmm, + } +} + +/// Hides `paths`, absolute POSIX paths that [`host`] places, in the child `index` of `plan`, and +/// pins with made-up identities those that a workload could move, as the planner does: those +/// strictly inside a writable child without write paths, or strictly inside one of its write +/// paths, which callers set first. +pub(super) fn deny(plan: &mut ExportPlan, index: usize, paths: &[&str]) { + let child = &mut plan.children[index]; + child.denied = paths.iter().map(|path| host(path)).collect(); + child.denied_identities = (1..) + .zip(&child.denied) + .map(|(number, path)| { + let moves = child.writable + && *path != child.root + && (child.write.is_empty() + || child + .write + .iter() + .any(|write| path != write && path.starts_with(write))); + moves.then_some(FileIdentity { + device: 1, + index: number, + }) + }) + .collect(); } fn message(result: Result) -> String { @@ -66,7 +139,6 @@ fn launch_contract_builds_agent_command_line() { workload_identity: (1000, 1001), memory_max: Some(Int::from(268_435_456_u32)), pids_max: Some(Int::from(64_u32)), - mounts: Vec::new(), }, ) .unwrap(); @@ -118,7 +190,7 @@ fn launch_contract_rejects_duplicates_and_reserved_tokens() { .unwrap_err(); assert_eq!(error.kind(), ErrorKind::Unsupported); assert_eq!(error.to_string(), "duplicate sandbox layer role(s): distro"); - let launch = with_mounts(Vec::new()).unwrap(); + let launch = with_export(None).unwrap(); assert_eq!( message(launch.plan().kernel_command_line("nvx_sandbox=0")), "nvx_sandbox is owned by the sandbox profile" @@ -286,7 +358,14 @@ fn workload_identities_parse_as_python_int() { #[test] fn launch_contract_attaches_mount_and_reserves_bootstrap_tokens() { - let launch = with_mounts(vec![mount("/workspace,share,rw", &["secrets"])]).unwrap(); + let mut plan = exported(&[( + "/workspace", + "/host/share", + MountAccess::ReadWrite, + MountKind::Directory, + )]); + deny(&mut plan, 0, &["/host/share/secrets"]); + let launch = with_export(Some(plan)).unwrap(); let arguments = launch.plan().openvmm_arguments().unwrap(); let start = arguments .iter() @@ -294,7 +373,12 @@ fn launch_contract_attaches_mount_and_reserves_bootstrap_tokens() { .unwrap(); assert_eq!( arguments[start..], - ["--mount", "/workspace,share,rw", "--mount-deny", "secrets"] + [ + "--mount".to_owned(), + format!("/workspace,{},rw", host_text("/host/share")), + "--mount-deny".to_owned(), + host_text("/host/share/secrets"), + ] ); assert!( !launch @@ -313,12 +397,14 @@ fn launch_contract_attaches_mount_and_reserves_bootstrap_tokens() { format!("{name} is owned by the sandbox --mount option") ); } - let base = with_mounts(Vec::new()) + let base = with_export(None) .unwrap() .plan() .kernel_command_line("") .unwrap(); - let fragment = mount::mounts_command_line_fragment(launch.mounts()); + let fragment = mount::boot_shares(launch.export().unwrap()) + .unwrap() + .fragment; assert_eq!( fragment, " virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw" @@ -332,49 +418,61 @@ fn launch_contract_attaches_mount_and_reserves_bootstrap_tokens() { #[test] fn launch_contract_attaches_several_shares_as_an_aggregate() { - let launch = with_mounts(vec![ - mount("/workspace,work,rw", &["secrets"]), - mount("/opt/hostedtoolcache,tools,ro", &["credentials"]), - mount("/srv/cache,cache,ro", &[]), - ]) - .unwrap(); - let names: Vec = launch - .mounts() + let shares = [ + ( + "/workspace", + "/host/work", + MountAccess::ReadWrite, + MountKind::Directory, + ), + ( + "/opt/hostedtoolcache", + "/host/tools", + MountAccess::ReadOnly, + MountKind::Directory, + ), + ( + "/srv/cache", + "/host/cache", + MountAccess::ReadOnly, + MountKind::Directory, + ), + ]; + let mut plan = exported(&shares); + deny(&mut plan, 0, &["/host/work/secrets"]); + deny(&mut plan, 1, &["/host/tools/credentials"]); + let launch = with_export(Some(plan.clone())).unwrap(); + let names: Vec = shares .iter() .enumerate() - .map(|(index, mount)| mount::aggregate_child_name(index, mount.guest_target())) + .map(|(index, &(target, ..))| mount::aggregate_child_name(index, target)) .collect(); let arguments = launch.plan().openvmm_arguments().unwrap(); // Several shares are the children of one aggregate, and OpenVMM attributes each denied - // path to the child that contains it, so relative paths are joined to their share. + // path, which is absolute, to the child that contains it. assert!(!arguments.iter().any(|argument| argument == "--mount")); let start = arguments .iter() .position(|argument| argument == "--mount-aggregate") .unwrap(); - let directory = cwd().unwrap(); assert_eq!( arguments[start..], [ "--mount-aggregate".to_owned(), "/run/nvx/shares".to_owned(), "--mount-child".to_owned(), - format!("{},work,rw", names[0]), + format!("{},{},rw", names[0], host_text("/host/work")), "--mount-child".to_owned(), - format!("{},tools,ro", names[1]), + format!("{},{},ro", names[1], host_text("/host/tools")), "--mount-child".to_owned(), - format!("{},cache,ro", names[2]), + format!("{},{},ro", names[2], host_text("/host/cache")), "--mount-deny".to_owned(), - directory.join("work").join("secrets").as_str().to_owned(), + host_text("/host/work/secrets"), "--mount-deny".to_owned(), - directory - .join("tools") - .join("credentials") - .as_str() - .to_owned(), + host_text("/host/tools/credentials"), ] ); - let fragment = mount::mounts_command_line_fragment(launch.mounts()); + let fragment = mount::boot_shares(&plan).unwrap().fragment; assert_eq!( fragment, " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=rw virtfs_aggregate=1" @@ -391,7 +489,9 @@ fn launch_contract_attaches_several_shares_as_an_aggregate() { "{command_line}" ); assert_eq!( - mount::mounts_command_line_fragment(&launch.mounts()[1..]), + mount::boot_shares(&exported(&shares[1..])) + .unwrap() + .fragment, " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" ); assert_eq!( @@ -399,34 +499,24 @@ fn launch_contract_attaches_several_shares_as_an_aggregate() { "nvx_share is owned by the sandbox profile" ); // Every share's tokens count toward the x86 budget. - let base = with_mounts(Vec::new()) + let base = with_export(None) .unwrap() .plan() .kernel_command_line("") .unwrap(); - let shares = command_line.len() - base.len(); + let tokens = command_line.len() - base.len(); let fitting = - "x".repeat(SANDBOX_COMMAND_LINE_MAX_SIZE - base.len() - shares - fragment.len() - 2); + "x".repeat(SANDBOX_COMMAND_LINE_MAX_SIZE - base.len() - tokens - fragment.len() - 2); launch.plan().kernel_command_line(&fitting).unwrap(); assert!( message(launch.plan().kernel_command_line(&format!("{fitting}x"))).contains("1024-byte") ); - let caller: Vec = [("/workspace", "work"), ("/tools", "tools")] - .iter() - .map(|(target, path)| { - SandboxMount::new( - target, - path, - MountAccess::ReadOnly, - MountPolicy::default(), - MountOwner::Caller, - MountKind::Directory, - ) - .unwrap() - }) - .collect(); - let arguments = mount::mounts_openvmm_arguments(&caller).unwrap(); + let caller = ExportPlan { + owner: Owner::Caller, + ..exported(&shares[1..]) + }; + let arguments = mount::boot_shares(&caller).unwrap().arguments; assert_eq!( arguments .iter() @@ -443,12 +533,21 @@ fn launch_contract_attaches_several_shares_as_an_aggregate() { #[test] fn launch_bounds_shares_by_the_kernel_command_line() { let launch = |count: usize| { - with_mounts( - (0..count) - .map(|index| mount(&format!("/share/{index:02},host{index}"), &[])) - .collect(), - ) - .unwrap() + let shares: Vec<(String, String)> = (0..count) + .map(|index| (format!("/share/{index:02}"), format!("/host{index}"))) + .collect(); + let shares: Vec<_> = shares + .iter() + .map(|(target, root)| { + ( + target.as_str(), + root.as_str(), + MountAccess::ReadOnly, + MountKind::Directory, + ) + }) + .collect(); + with_export(Some(exported(&shares))).unwrap() }; // No fixed number of shares is the limit, only the command line. launch(10).plan().kernel_command_line("").unwrap(); @@ -483,29 +582,24 @@ fn launch_rejects_invalid_share_sets() { "every sandbox --mount must use the same --mount-owner", ), ] { - assert_eq!(message(with_mounts(mounts)), expected); + assert_eq!(message(mount::validate_mounts(&mounts)), expected); } // A shared prefix that is not a path component is not an overlap. - with_mounts(vec![vmm("/work,a,rw"), vmm("/workspace,b")]).unwrap(); + mount::validate_mounts(&[vmm("/work,a,rw"), vmm("/workspace,b")]).unwrap(); } #[test] fn launch_contract_attaches_a_file_as_an_aggregate_child() { - let file = |value: &str| { - SandboxMount::parse( - value, - MountPolicy::default(), - MountOwner::Vmm, - MountKind::File, - ) - .unwrap() - }; - let settings = file("/etc/app/settings.json,settings.json,ro"); - let launch = with_mounts(vec![settings.clone()]).unwrap(); + let settings = ( + "/etc/app/settings.json", + "/host/settings.json", + MountAccess::ReadOnly, + MountKind::File, + ); + let launch = with_export(Some(exported(&[settings]))).unwrap(); let name = mount::aggregate_child_name(0, "/etc/app/settings.json"); // Even alone, a file is the child of an aggregate, because the guest mounts the device's // root, which is a directory. - assert!(mount::mounts_use_aggregate(launch.mounts())); let arguments = launch.plan().openvmm_arguments().unwrap(); assert!(!arguments.iter().any(|argument| argument == "--mount")); let start = arguments @@ -518,11 +612,13 @@ fn launch_contract_attaches_a_file_as_an_aggregate_child() { "--mount-aggregate".to_owned(), "/run/nvx/shares".to_owned(), "--mount-child".to_owned(), - format!("{name},settings.json,ro,file"), + format!("{name},{},ro,file", host_text("/host/settings.json")), ] ); assert_eq!( - mount::mounts_command_line_fragment(launch.mounts()), + mount::boot_shares(launch.export().unwrap()) + .unwrap() + .fragment, " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" ); assert!( @@ -534,14 +630,28 @@ fn launch_contract_attaches_a_file_as_an_aggregate_child() { ); // Files and directories share one aggregate, and only a file's token is marked. - let workspace = mount("/workspace,work,rw", &["secrets"]); - let output = file("/results/output.txt,output.txt,rw"); - let mixed = with_mounts(vec![workspace, settings, output]).unwrap(); - let names: Vec = mixed - .mounts() + let shares = [ + ( + "/workspace", + "/host/work", + MountAccess::ReadWrite, + MountKind::Directory, + ), + settings, + ( + "/results/output.txt", + "/host/output.txt", + MountAccess::ReadWrite, + MountKind::File, + ), + ]; + let mut plan = exported(&shares); + deny(&mut plan, 0, &["/host/work/secrets"]); + let mixed = with_export(Some(plan)).unwrap(); + let names: Vec = shares .iter() .enumerate() - .map(|(index, mount)| mount::aggregate_child_name(index, mount.guest_target())) + .map(|(index, &(target, ..))| mount::aggregate_child_name(index, target)) .collect(); assert!( mixed @@ -559,10 +669,7 @@ fn launch_contract_attaches_a_file_as_an_aggregate_child() { .iter() .position(|argument| argument == "--mount-deny") .unwrap(); - assert_eq!( - arguments[denied + 1], - cwd().unwrap().join("work").join("secrets").as_str() - ); + assert_eq!(arguments[denied + 1], host_text("/host/work/secrets")); // A file's mode applies to the whole file, so no policy path names one. for policy in [ @@ -648,7 +755,7 @@ impl Fixture { root.join(name).as_str().to_owned() } - fn launch(&self, mounts: Vec) -> SandboxLaunch { + fn launch(&self) -> SandboxLaunch { SandboxLaunch::new( vec![SandboxLayer::new( LayerRole::Distro, @@ -656,13 +763,16 @@ impl Fixture { "11111111-1111-1111-1111-111111111111", )], &self.path("scratch.ext4"), - SandboxSettings { - mounts, - ..SandboxSettings::default() - }, + SandboxSettings::default(), ) .unwrap() } + + /// Returns the launch with the export plan of `mounts`, which [`mount::plan_mounts`] checks + /// against the host. + fn planned(&self, mounts: Vec) -> Result { + self.launch().with_export(mount::plan_mounts(&mounts)?) + } } fn parse(value: &str, policy: MountPolicy) -> SandboxMount { @@ -700,8 +810,7 @@ fn launch_validation_rejects_overlapping_or_foreign_host_paths() { MountPolicy::default(), ); let error = fixture - .launch(vec![workspace.clone(), toolcache]) - .validated() + .planned(vec![workspace.clone(), toolcache]) .unwrap_err(); assert!(error.to_string().contains("overlap"), "{error}"); } @@ -716,8 +825,7 @@ fn launch_validation_rejects_overlapping_or_foreign_host_paths() { MountPolicy::default(), ); let error = fixture - .launch(vec![foreign, toolcache.clone()]) - .validated() + .planned(vec![foreign, toolcache.clone()]) .unwrap_err(); assert_eq!( error.to_string(), @@ -729,40 +837,53 @@ fn launch_validation_rejects_overlapping_or_foreign_host_paths() { // A denied path may name a share's root, which OpenVMM then hides behind its allowed paths, // but no allowed or writable path may. fixture - .launch(vec![ + .planned(vec![ parse( &format!("/workspace,{work},rw"), policy(&["."], &[&nested], &[]), ), toolcache.clone(), ]) - .validated() .unwrap(); for (allowed, writable) in [(&["."][..], &[][..]), (&[], &["."])] { let error = fixture - .launch(vec![ + .planned(vec![ parse( &format!("/workspace,{work},rw"), policy(&["nested"], allowed, writable), ), toolcache.clone(), ]) - .validated() .unwrap_err(); assert!(error.to_string().contains("not inside"), "{error}"); } - let validated = fixture.launch(vec![ - parse( + let validated = fixture + .planned(vec![ + parse( + &format!("/workspace,{work},rw"), + policy(&["nested"], &[], &[]), + ), + parse( + &format!("/opt/hostedtoolcache,{tools}"), + policy(&[&credentials], &[], &[]), + ), + ]) + .unwrap(); + validated.validated().unwrap(); + assert_eq!(validated.export().unwrap().children.len(), 2); + // One share's policy paths must lie in it too. + let error = fixture + .planned(vec![parse( &format!("/workspace,{work},rw"), - policy(&["nested"], &[], &[]), - ), - parse( - &format!("/opt/hostedtoolcache,{tools}"), policy(&[&credentials], &[], &[]), - ), - ]); - validated.validated().unwrap(); - assert_eq!(validated.mounts().len(), 2); + )]) + .unwrap_err(); + assert_eq!( + error.to_string(), + format!( + "--mount-deny {credentials} is not inside the host directory of --mount /workspace" + ) + ); // Resolving a path doesn't see through a bind mount, so one directory reached through two // paths, or a share inside a bind mount of the other share's directory, is rejected by its @@ -803,12 +924,111 @@ fn launch_validation_rejects_overlapping_or_foreign_host_paths() { } } +#[cfg(windows)] +#[test] +fn shares_that_only_a_verbatim_path_names_are_refused() { + let fixture = Fixture::new(); + fs::create_dir_all(fixture.path("work")).unwrap(); + // Without the verbatim prefix, Windows drops the trailing period of `work.`, so the share + // would export `work` instead, read-write. + let verbatim = fs::canonicalize(fixture.path("")).unwrap(); + let host = format!(r"{}\work.", verbatim.display()); + fs::create_dir(&host).unwrap(); + let error = fixture + .planned(vec![parse( + &format!("/workspace,{host},rw"), + MountPolicy::default(), + )]) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!(error.to_string().contains("is not accessible"), "{error}"); +} + +#[test] +fn launch_validation_rejects_shares_that_came_to_overlap() { + let fixture = Fixture::new(); + fs::write(fixture.path("settings.json"), b"{}").unwrap(); + fs::write(fixture.path("token"), b"token").unwrap(); + let file = |target: &str, name: &str| { + SandboxMount::parse( + &format!("{target},{}", fixture.path(name)), + MountPolicy::default(), + MountOwner::Vmm, + MountKind::File, + ) + .unwrap() + }; + let launch = fixture + .planned(vec![ + file("/srv/settings.json", "settings.json"), + file("/srv/token", "token"), + ]) + .unwrap(); + launch.validated().unwrap(); + // A hard link that replaced one shared file with the other after provisioning would export + // one file twice. + fs::remove_file(fixture.path("token")).unwrap(); + fs::hard_link(fixture.path("settings.json"), fixture.path("token")).unwrap(); + let error = launch.validated().unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!( + error + .to_string() + .contains("name the same directory or file"), + "{error}" + ); + + // A bind mount that made one shared directory a view of the directory that holds the other + // after provisioning would let one share reach the other's files under its access mode. + fs::create_dir_all(fixture.path("tools")).unwrap(); + fs::create_dir_all(fixture.path("work")).unwrap(); + let launch = fixture + .planned(vec![ + parse( + &format!("/workspace,{},rw", fixture.path("work")), + MountPolicy::default(), + ), + parse( + &format!("/opt/tools,{}", fixture.path("tools")), + MountPolicy::default(), + ), + ]) + .unwrap(); + launch.validated().unwrap(); + let roots: Vec = launch + .export() + .unwrap() + .children + .iter() + .map(|child| PurePath::native(child.root.to_str().unwrap())) + .collect(); + let (view, holder) = (roots[0].clone(), roots[1].parents().next().unwrap()); + let bound = move |path: &PurePath| -> std::io::Result<(u64, u128)> { + if *path == view || *path == holder { + return Ok((1, 1)); + } + Ok((0, u128::from(path_hash(path.as_str())))) + }; + let queries = mount::HostQueries { + identity: &bound, + ..mount::HostQueries::native() + }; + let error = launch.plan().validated_with(&queries).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert!( + error + .to_string() + .contains("inside the other through a bind mount"), + "{error}" + ); +} + #[test] fn launch_validation_requires_plain_mount_directory() { let fixture = Fixture::new(); let share = fixture.path("share"); let launch = || { - fixture.launch(vec![ + fixture.planned(vec![ SandboxMount::new( "/workspace", &share, @@ -821,14 +1041,20 @@ fn launch_validation_requires_plain_mount_directory() { ]) }; let expected = format!("sandbox mount host path is not a plain directory: {share}"); - assert_eq!(launch().validated().unwrap_err().to_string(), expected); + assert_eq!(launch().unwrap_err().to_string(), expected); fs::write(&share, b"file").unwrap(); - assert_eq!(launch().validated().unwrap_err().to_string(), expected); + assert_eq!(launch().unwrap_err().to_string(), expected); fs::remove_file(&share).unwrap(); fs::create_dir(&share).unwrap(); - let validated = launch(); + let validated = launch().unwrap(); validated.validated().unwrap(); - assert_eq!(validated.mounts()[0].host_path(), Path::new(&share)); + let canonical = fs::canonicalize(&share).unwrap(); + // The planner leaves out Windows' verbatim prefix. + let canonical = canonical + .to_str() + .and_then(|text| text.strip_prefix(r"\\?\")) + .map_or(canonical.clone(), PathBuf::from); + assert_eq!(validated.export().unwrap().children[0].root, canonical); } #[test] @@ -937,7 +1163,7 @@ fn launch_validation_requires_the_layers_and_the_scratch_image() { fixture.path("missing.erofs") ) ); - let scratch = fixture.launch(Vec::new()); + let scratch = fixture.launch(); fs::remove_file(fixture.path("scratch.ext4")).unwrap(); assert_eq!( scratch.validated().unwrap_err().to_string(), diff --git a/nvx-rs/tests/cli.rs b/nvx-rs/tests/cli.rs index 4c8c3fc12..46905e516 100644 --- a/nvx-rs/tests/cli.rs +++ b/nvx-rs/tests/cli.rs @@ -10,10 +10,10 @@ //! `nvx-rs --root ROOT`, and every step must exit with the recorded status, print the recorded //! output, and leave the recorded files, after the root, process IDs, and random identifiers are //! replaced with the placeholders that the scripts used, and the devices, file IDs, and times -//! that the bindings of `config.json` record, and the numbers of the descriptors that name a -//! launch's checked files, with `{int}` and `{hex32}` as well. Now that `nvx.py run` and -//! `nvx.py sandbox` run `nvx-rs`, the recordings are their reference: a deliberate change of -//! behavior updates them by hand. +//! that the bindings of `config.json` record, the file identities that its export plan records, +//! and the numbers of the descriptors that name a launch's checked files, with `{int}` and +//! `{hex32}` as well. Now that `nvx.py run` and `nvx.py sandbox` run `nvx-rs`, the recordings are +//! their reference: a deliberate change of behavior updates them by hand. #![cfg(all( feature = "cli", feature = "testing", @@ -111,12 +111,14 @@ impl Root { text = text.replace(&escaped, "{root_json}"); } text = text.replace(raw, "{root}"); - // The seals of a distro binding hold the device, file ID, and times of the layer. + // The seals of a distro binding hold the device, file ID, and times of the layer, and an + // export plan the device and index of the objects that a workload could move. for prefix in [ "\"pid\": ", "\"start_time\": ", "\"startTime\": ", "\"device\": ", + "\"index\": ", "\"seconds\": ", "\"nanoseconds\": ", ] { diff --git a/nvx-rs/tests/fixtures/cli.json b/nvx-rs/tests/fixtures/cli.json index 67ea81642..885c781b1 100644 --- a/nvx-rs/tests/fixtures/cli.json +++ b/nvx-rs/tests/fixtures/cli.json @@ -117,7 +117,7 @@ {"name":"validation: relative mount target","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","data,{root}/share"],"status":1,"stdout":"","stderr":"error: invalid sandbox mount target 'data': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','\n"}]}, {"name":"validation: missing mount host","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/missing"],"status":1,"stdout":"","stderr":"error: sandbox mount host path is not a plain directory: {root}/missing\n"}]}, {"name":"validation: invalid mount mode","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rx"],"status":1,"stdout":"","stderr":"error: unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)\n"}]}, -{"name":"validation: deny outside share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount-deny","{root}/share2"],"status":2,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:/proc/self/fd/{int},ro --microvm-sandbox-block scratch:file:/proc/self/fd/{int} --microvm-workload-identity 65534:65534 --mount /data,{root}/share,ro --mount-deny {root}/share2 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":"openvmm: invalid --mount-deny {root}/share2\n"}]}, +{"name":"validation: deny outside share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount-deny","{root}/share2"],"status":1,"stdout":"","stderr":"error: --mount-deny {root}/share2 is not inside the host directory of --mount /data\n"}]}, {"name":"validation: allow outside denied path","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount-allow","{root}/share/hidden/shown"],"status":1,"stdout":"","stderr":"error: --mount-allow exposes a path inside a --mount-deny path of the same share, which has none\n"}]}, {"name":"validation: write in read-only share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,ro","--mount-write","{root}/share/writable"],"status":1,"stdout":"","stderr":"error: --mount-write narrows the writes of a read-write share; --mount /data is read-only\n"}]}, {"name":"validation: reserved command-line token","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--cmdline","nvx_sandbox=0"],"status":1,"stdout":"","stderr":"error: nvx_sandbox is owned by the sandbox profile\n"}]}, @@ -149,11 +149,11 @@ {"name":"run: missing kernel","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"remove":["build/vmlinux"],"status":1,"stdout":"","stderr":"error: Linux direct kernel not found: {root}/build/vmlinux\n"}]}, {"name":"run: missing initramfs","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"remove":["build/initramfs.cpio.gz"],"status":1,"stdout":"","stderr":"error: initramfs not found: {root}/build/initramfs.cpio.gz\n"}]}, {"name":"lifecycle: minimal","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":"hello world\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/tmp"],"status":0,"stdout":"/tmp\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/sh","--arg=-c","--arg","echo out; echoerr err; exit 3","--outcome-report","{root}/reports/exec.json"],"check":["reports/exec.json"],"status":3,"stdout":"out\n","stderr":"err\n","files":{"reports/exec.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"exit\",\n \"operation\": \"exec\",\n \"status_code\": 3\n },\n \"schema_version\": 1\n}\n","mode":384}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/env","--environment","A=1","--environment","B=two"],"status":0,"stdout":"A=1\nB=two\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/printenv","--arg","PATH","--inherit-default-environment","--environment","A=1"],"status":0,"stdout":"/usr/sbin:/usr/bin:/sbin:/bin\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/env","--environment-file","{root}/env.json"],"write":{"env.json":"[\"X=1\", \"Y=two words\"]"},"status":0,"stdout":"X=1\nY=two words\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/env","--environment-file","{root}/empty.json"],"write":{"empty.json":"[]"},"status":0,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/sh","--arg=-c","--arg","sleep 5000","--exec-timeout-ms","50","--outcome-report","{root}/reports/exec-timeout.json"],"check":["reports/exec-timeout.json"],"status":124,"stdout":"","stderr":"","files":{"reports/exec-timeout.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"timeout\",\n \"operation\": \"exec\",\n \"status_code\": 124\n },\n \"schema_version\": 1\n}\n","mode":384}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/sh","--arg=-c","--arg","signal 9"],"status":137,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/missing"],"status":127,"stdout":"","stderr":"openvmm: /bin/missing: not found\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/missing"],"status":1,"stdout":"","stderr":"error: managed guest rejected exec (status=2, category=cwd-failed)\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","relative"],"status":1,"stdout":"","stderr":"error: managed exec working directory must be an absolute path of at most 4096 bytes\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/echo","--outcome-report","{root}/reports/exec.json"],"status":1,"stdout":"","stderr":"error: outcome report already exists: {root}/reports/exec.json\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/echo","--outcome-report","{root}/missing/exec.json"],"status":1,"stdout":"","stderr":"error: outcome report parent is not a plain directory: {root}/missing\n"},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: one share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 2,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 2,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,ro\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/data"],"status":0,"stdout":"/data\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 2,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 2,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 2,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: caller-owned share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-owner","caller"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 3,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 3,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,rw\",\"--mount-owner\",\"caller\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 3,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 3,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 3,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: several shares","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount","/other,{root}/share2,rw"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 4,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 4,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-bd47413b5c03dfc6e4b5b8143fe205ea,{root}/share,ro\",\"--mount-child\",\"1-bf2faee2596026620b5cbc997c2d7d45,{root}/share2,rw\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,ro nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,rw\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/other"],"status":0,"stdout":"/other\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 4,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 4,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 4,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: share policies","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-deny","{root}/share/hidden","--mount-allow","{root}/share/hidden/shown","--mount-write","{root}/share/writable"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 5,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 5,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,rw\",\"--mount-deny\",\"{root}/share/hidden\",\"--mount-allow\",\"{root}/share/hidden/shown\",\"--mount-write\",\"{root}/share/writable\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 5,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 5,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 5,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: file share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/etc/single.txt,{root}/single.txt,ro"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 6,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 6,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root}/single.txt,ro,file\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 6,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 6,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 6,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: one share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,ro\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/data"],"status":0,"stdout":"/data\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: caller-owned share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-owner","caller"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": []\n }\n ],\n \"owner\": \"caller\"\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": []\n }\n ],\n \"owner\": \"caller\"\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,rw\",\"--mount-owner\",\"caller\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": []\n }\n ],\n \"owner\": \"caller\"\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": []\n }\n ],\n \"owner\": \"caller\"\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": []\n }\n ],\n \"owner\": \"caller\"\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: several shares","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount","/other,{root}/share2,rw"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n },\n {\n \"child\": 1,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/other\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n },\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share2\",\n \"writable\": true,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n },\n {\n \"child\": 1,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/other\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n },\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share2\",\n \"writable\": true,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-bd47413b5c03dfc6e4b5b8143fe205ea,{root}/share,ro\",\"--mount-child\",\"1-bf2faee2596026620b5cbc997c2d7d45,{root}/share2,rw\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,ro nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,rw\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/other"],"status":0,"stdout":"/other\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n },\n {\n \"child\": 1,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/other\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n },\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share2\",\n \"writable\": true,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n },\n {\n \"child\": 1,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/other\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n },\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share2\",\n \"writable\": true,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/data\"\n },\n {\n \"child\": 1,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/other\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share\",\n \"writable\": false,\n \"write\": []\n },\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"root\": \"{root}/share2\",\n \"writable\": true,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: share policies","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-deny","{root}/share/hidden","--mount-allow","{root}/share/hidden/shown","--mount-write","{root}/share/writable"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied\": [\n \"{root}/share/hidden\"\n ],\n \"deniedIdentities\": [\n null\n ],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": [\n \"{root}/share/writable\"\n ]\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied\": [\n \"{root}/share/hidden\"\n ],\n \"deniedIdentities\": [\n null\n ],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": [\n \"{root}/share/writable\"\n ]\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,rw\",\"--mount-deny\",\"{root}/share/hidden\",\"--mount-allow\",\"{root}/share/hidden/shown\",\"--mount-write\",\"{root}/share/writable\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied\": [\n \"{root}/share/hidden\"\n ],\n \"deniedIdentities\": [\n null\n ],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": [\n \"{root}/share/writable\"\n ]\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied\": [\n \"{root}/share/hidden\"\n ],\n \"deniedIdentities\": [\n null\n ],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": [\n \"{root}/share/writable\"\n ]\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": false,\n \"source\": \"\",\n \"target\": \"/data\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied\": [\n \"{root}/share/hidden\"\n ],\n \"deniedIdentities\": [\n null\n ],\n \"root\": \"{root}/share\",\n \"writable\": true,\n \"write\": [\n \"{root}/share/writable\"\n ]\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: file share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/etc/single.txt,{root}/single.txt,ro"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/etc/single.txt\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"file\": true,\n \"root\": \"{root}/single.txt\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/etc/single.txt\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"file\": true,\n \"root\": \"{root}/single.txt\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root}/single.txt,ro,file\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/etc/single.txt\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"file\": true,\n \"root\": \"{root}/single.txt\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/etc/single.txt\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"file\": true,\n \"root\": \"{root}/single.txt\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"export\": {\n \"binds\": [\n {\n \"child\": 0,\n \"readOnly\": true,\n \"source\": \"\",\n \"target\": \"/etc/single.txt\"\n }\n ],\n \"children\": [\n {\n \"allowed\": [],\n \"denied\": [],\n \"deniedIdentities\": [],\n \"file\": true,\n \"root\": \"{root}/single.txt\",\n \"writable\": false,\n \"write\": []\n }\n ]\n },\n \"format\": 7,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, {"name":"lifecycle: network and launch options","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/server","--arg=--serve","--hostname","worker","--workload-user","1000:1000","--memory-max","268435456","--pids-max","64","--memory-mib","384","--hypervisor","auto","--cmdline","quiet","--net","10.0.0.2/24","--network-profile","portable","--network-egress","allow","--network-ingress","deny","--network-egress-policy-file","{root}/policy.json","--host-loopback","allow","--host-loopback-forward","tcp:18080:8080","--host-loopback-forward","udp:19090:9090"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 443}], \"deny\": [{\"cidr\": \"192.0.2.128/25\"}]}"},"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"mode":384,"text":"{\n \"cmdline\": \"quiet\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"network\": {\n \"allow\": {\n \"name\": \"--network-egress-allow\",\n \"rules\": [\n {\n \"ports\": [\n {\n \"endPort\": null,\n \"port\": 443,\n \"protocol\": \"tcp\"\n }\n ],\n \"to\": [\n {\n \"cidr\": \"192.0.2.0/24\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"deny\": {\n \"name\": \"--network-egress-deny\",\n \"rules\": [\n {\n \"ports\": [],\n \"to\": [\n {\n \"cidr\": \"192.0.2.128/25\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"egress\": \"allow\",\n \"guestNetwork\": \"10.0.0.2/24\",\n \"hostLoopback\": \"allow\",\n \"hostLoopbackForwards\": [\n {\n \"guestPort\": 8080,\n \"hostPort\": 18080,\n \"protocol\": \"tcp\"\n },\n {\n \"guestPort\": 9090,\n \"hostPort\": 19090,\n \"protocol\": \"udp\"\n }\n ],\n \"ingress\": \"deny\",\n \"proxy\": null\n },\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"mode":384,"text":"{\n \"cmdline\": \"quiet\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"network\": {\n \"allow\": {\n \"name\": \"--network-egress-allow\",\n \"rules\": [\n {\n \"ports\": [\n {\n \"endPort\": null,\n \"port\": 443,\n \"protocol\": \"tcp\"\n }\n ],\n \"to\": [\n {\n \"cidr\": \"192.0.2.0/24\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"deny\": {\n \"name\": \"--network-egress-deny\",\n \"rules\": [\n {\n \"ports\": [],\n \"to\": [\n {\n \"cidr\": \"192.0.2.128/25\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"egress\": \"allow\",\n \"guestNetwork\": \"10.0.0.2/24\",\n \"hostLoopback\": \"allow\",\n \"hostLoopbackForwards\": [\n {\n \"guestPort\": 8080,\n \"hostPort\": 18080,\n \"protocol\": \"tcp\"\n },\n {\n \"guestPort\": 9090,\n \"hostPort\": 19090,\n \"protocol\": \"udp\"\n }\n ],\n \"ingress\": \"deny\",\n \"proxy\": null\n },\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"1000:1000\",\"--net\",\"10.0.0.2/24\",\"--network-profile\",\"portable\",\"--network-egress\",\"allow\",\"--network-ingress\",\"deny\",\"--host-loopback\",\"allow\",\"--network-egress-allow\",\"192.0.2.0/24:tcp:443\",\"--network-egress-deny\",\"192.0.2.128/25\",\"--host-loopback-forward\",\"tcp:18080:8080\",\"--host-loopback-forward\",\"udp:19090:9090\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"384M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=worker nvx_memory_max=268435456 nvx_pids_max=65\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"mode":384,"text":"{\n \"cmdline\": \"quiet\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"network\": {\n \"allow\": {\n \"name\": \"--network-egress-allow\",\n \"rules\": [\n {\n \"ports\": [\n {\n \"endPort\": null,\n \"port\": 443,\n \"protocol\": \"tcp\"\n }\n ],\n \"to\": [\n {\n \"cidr\": \"192.0.2.0/24\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"deny\": {\n \"name\": \"--network-egress-deny\",\n \"rules\": [\n {\n \"ports\": [],\n \"to\": [\n {\n \"cidr\": \"192.0.2.128/25\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"egress\": \"allow\",\n \"guestNetwork\": \"10.0.0.2/24\",\n \"hostLoopback\": \"allow\",\n \"hostLoopbackForwards\": [\n {\n \"guestPort\": 8080,\n \"hostPort\": 18080,\n \"protocol\": \"tcp\"\n },\n {\n \"guestPort\": 9090,\n \"hostPort\": 19090,\n \"protocol\": \"udp\"\n }\n ],\n \"ingress\": \"deny\",\n \"proxy\": null\n },\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","openvmm.log","runtime.json","sandbox.lock"],"mode":448},"state/config.json":{"mode":384,"text":"{\n \"cmdline\": \"quiet\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"network\": {\n \"allow\": {\n \"name\": \"--network-egress-allow\",\n \"rules\": [\n {\n \"ports\": [\n {\n \"endPort\": null,\n \"port\": 443,\n \"protocol\": \"tcp\"\n }\n ],\n \"to\": [\n {\n \"cidr\": \"192.0.2.0/24\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"deny\": {\n \"name\": \"--network-egress-deny\",\n \"rules\": [\n {\n \"ports\": [],\n \"to\": [\n {\n \"cidr\": \"192.0.2.128/25\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"egress\": \"allow\",\n \"guestNetwork\": \"10.0.0.2/24\",\n \"hostLoopback\": \"allow\",\n \"hostLoopbackForwards\": [\n {\n \"guestPort\": 8080,\n \"hostPort\": 18080,\n \"protocol\": \"tcp\"\n },\n {\n \"guestPort\": 9090,\n \"hostPort\": 19090,\n \"protocol\": \"udp\"\n }\n ],\n \"ingress\": \"deny\",\n \"proxy\": null\n },\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"{root}/state/control.sock\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"],"mode":448},"state/config.json":{"mode":384,"text":"{\n \"cmdline\": \"quiet\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"network\": {\n \"allow\": {\n \"name\": \"--network-egress-allow\",\n \"rules\": [\n {\n \"ports\": [\n {\n \"endPort\": null,\n \"port\": 443,\n \"protocol\": \"tcp\"\n }\n ],\n \"to\": [\n {\n \"cidr\": \"192.0.2.0/24\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"deny\": {\n \"name\": \"--network-egress-deny\",\n \"rules\": [\n {\n \"ports\": [],\n \"to\": [\n {\n \"cidr\": \"192.0.2.128/25\",\n \"except\": []\n }\n ]\n }\n ]\n },\n \"egress\": \"allow\",\n \"guestNetwork\": \"10.0.0.2/24\",\n \"hostLoopback\": \"allow\",\n \"hostLoopbackForwards\": [\n {\n \"guestPort\": 8080,\n \"hostPort\": 18080,\n \"protocol\": \"tcp\"\n },\n {\n \"guestPort\": 9090,\n \"hostPort\": 19090,\n \"protocol\": \"udp\"\n }\n ],\n \"ingress\": \"deny\",\n \"proxy\": null\n },\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, {"name":"lifecycle: proxy-only network","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--host-loopback","deny","--network-proxy","10.0.0.1:3128"],"check":["state/config.json"],"status":0,"stdout":"","stderr":"","files":{"state/config.json":{"mode":384,"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": {\n \"allow\": {\n \"name\": \"--network-egress-allow\",\n \"rules\": []\n },\n \"deny\": {\n \"name\": \"--network-egress-deny\",\n \"rules\": []\n },\n \"egress\": \"deny\",\n \"guestNetwork\": \"10.0.0.2/24\",\n \"hostLoopback\": \"deny\",\n \"hostLoopbackForwards\": [],\n \"ingress\": \"deny\",\n \"proxy\": {\n \"port\": 3128,\n \"scheme\": null\n }\n },\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n"}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:/proc/self/fd/{int},ro\",\"--microvm-sandbox-block\",\"scratch:file:/proc/self/fd/{int}\",\"--microvm-workload-identity\",\"65534:65534\",\"--net\",\"10.0.0.2/24\",\"--network-profile\",\"portable\",\"--network-egress\",\"deny\",\"--network-ingress\",\"deny\",\"--host-loopback\",\"deny\",\"--network-proxy\",\"10.0.0.1:3128\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, {"name":"state: operations without a sandbox","steps":[{"argv":["sandbox","start","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"},{"argv":["sandbox","exec","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"},{"argv":["sandbox","stop","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"}]}, @@ -161,7 +161,7 @@ {"name":"state: relative and nested state directory","steps":[{"argv":["sandbox","provision","--state-dir","nested/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"check":["nested/state","nested/state/config.json"],"status":0,"stdout":"","stderr":"","files":{"nested/state":{"entries":["config.json","sandbox.lock"],"mode":448},"nested/state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384}}},{"argv":["sandbox","deprovision","--state-dir","nested/state"],"check":["nested/state"],"status":0,"stdout":"","stderr":"","files":{"nested/state":null}}]}, {"name":"state: failed start","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--cmdline","fake_exit_on_start=5"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\n","files":{"state":{"entries":["config.json","openvmm.log","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"fake_exit_on_start=5\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\n"}}},{"argv":["sandbox","start","--state-dir","{root}/state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\n","files":{"state":{"entries":["config.json","openvmm.log","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"fake_exit_on_start=5\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\n"}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, {"name":"state: start without OpenVMM","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state"],"remove":["openvmm/target/release/openvmm{exe}"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM release binary not found: {root}/openvmm/target/release/openvmm\n","files":{"state":{"entries":["config.json","sandbox.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"distro_binding\": {\n \"format\": \"erofs\",\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\n \"image\": {\n \"changed\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 7,\n \"modified\": {\n \"nanoseconds\": {int},\n \"seconds\": {int}\n },\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"manifest\": null,\n \"policy\": {\n \"result\": \"accepted\",\n \"revision\": 1\n },\n \"version\": 1\n },\n \"format\": 1,\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"network\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"scratch_binding\": {\n \"device\": {int},\n \"fileId\": \"{hex32}\",\n \"kind\": \"regular\",\n \"length\": 8,\n \"scheme\": \"linux\",\n \"version\": 1\n },\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"status":0,"stdout":"","stderr":""}]}, -{"name":"state: malformed configuration","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"{\"format\": 7}"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}/state/config.json\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"[]"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}/state/config.json\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"{\"format\": 1}"},"status":1,"stdout":"","stderr":"error: sandbox configuration is malformed\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, +{"name":"state: malformed configuration","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"{\"format\": 8}"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}/state/config.json\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"[]"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}/state/config.json\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"{\"format\": 1}"},"status":1,"stdout":"","stderr":"error: sandbox configuration is malformed\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, {"name":"state: not a directory","steps":[{"argv":["sandbox","start","--state-dir","{root}/single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/single.txt\n"},{"argv":["sandbox","provision","--state-dir","{root}/single.txt","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: [Errno 17] File exists: '{root}/single.txt'\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/single.txt\n"}]}, {"name":"state: foreign file","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"write":{"state/foreign.txt":"x"},"check":["state"],"status":1,"stdout":"","stderr":"error: sandbox state directory contains files not owned by NVX: foreign.txt\n","files":{"state":{"entries":["foreign.txt"],"mode":448}}}]}, {"name":"run, usage: help","steps":[{"argv":["run","-h"],"status":0,"stdout":"usage: nvx-rs run [-h] [--guest {alpine,ubuntu,azurelinux}]\n [--hypervisor {auto,whp,kvm,mshv}] [--machine {microvm}]\n [--memory-mib MEMORY_MIB]\n [--memory-capacity-mib MEMORY_CAPACITY_MIB]\n [--processors {1,2,4,8}] [--cpu-profile ID]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny MOUNT_DENY] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--restore-snapshot RESTORE_SNAPSHOT]\n [--restore-processors {1,2,4,8}]\n [--restore-memory-mib RESTORE_MEMORY_MIB]\n [--restore-ready-path RESTORE_READY_PATH] [--dry-run]\n\noptions:\n -h, --help show this help message and exit\n --guest {alpine,ubuntu,azurelinux}\n --hypervisor {auto,whp,kvm,mshv}\n --machine {microvm}\n --memory-mib MEMORY_MIB\n guest RAM in MiB (default by --guest: alpine 128,\n ubuntu 512, azurelinux 512)\n --memory-capacity-mib MEMORY_CAPACITY_MIB\n --processors {1,2,4,8}\n --cpu-profile ID OpenVMM CPU profile: auto (OpenVMM's default) for the\n built-in profile of the host's CPU, a built-in profile\n ID, or host to derive a development profile from this\n host (doc/usage.md)\n --mount GUEST_TARGET,HOST_PATH[,ro|rw]\n live-share a host directory, or a single regular host\n file without its directory; the mode follows the last\n comma, so HOST_PATH may contain commas when the mode\n is given; repeat for more shares, each with its own\n guest target and mode, as children of one virtio-fs\n device\n --mount-deny MOUNT_DENY\n --mount-allow HOST_PATH\n expose one existing path inside a --mount-deny path\n again\n --mount-write HOST_PATH\n make one existing path one of the only writable parts\n of a read-write --mount\n --mount-owner {vmm,caller}\n host identity of the share's file operations: vmm\n (default) or caller, with guest root squashed to the\n owner of the shared directory or file (Linux only)\n --net IPV4/PREFIX\n --network-profile {portable}\n --network-egress {allow,deny}\n --network-ingress {allow,deny}\n --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]\n allow guest egress to an IPv4 or IPv6 CIDR, optionally\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\n udp only on PORT or on the inclusive range PORT-PORT;\n repeat to add rules\n --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]\n deny guest egress to an IPv4 or IPv6 CIDR, optionally\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\n udp only on PORT or on the inclusive range PORT-PORT;\n repeat to add rules\n --network-egress-policy-file PATH\n load bounded MXC-shaped IPv4 and IPv6 egress rules\n from JSON\n --host-loopback {allow,deny}\n --network-proxy IPV4:TCP-PORT\n --host-loopback-forward HOST_LOOPBACK_FORWARD\n --outcome-report OUTCOME_REPORT\n write a bounded local JSON outcome report\n --cmdline CMDLINE\n --restore-snapshot RESTORE_SNAPSHOT\n --restore-processors {1,2,4,8}\n --restore-memory-mib RESTORE_MEMORY_MIB\n --restore-ready-path RESTORE_READY_PATH\n --dry-run\n","stderr":""}]}, @@ -432,7 +432,7 @@ {"name":"validation: relative mount target","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","data,{root}\\share"],"status":1,"stdout":"","stderr":"error: invalid sandbox mount target 'data': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','\r\n"}]}, {"name":"validation: missing mount host","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\missing"],"status":1,"stdout":"","stderr":"error: sandbox mount host path is not a plain directory: {root}\\missing\r\n"}]}, {"name":"validation: invalid mount mode","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rx"],"status":1,"stdout":"","stderr":"error: unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)\r\n"}]}, -{"name":"validation: deny outside share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount-deny","{root}\\share2"],"status":2,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:\\\\?\\{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:\\\\?\\{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}\\share,ro --mount-deny {root}\\share2 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":"openvmm: invalid --mount-deny {root}\\share2\n"}]}, +{"name":"validation: deny outside share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount-deny","{root}\\share2"],"status":1,"stdout":"","stderr":"error: --mount-deny {root}\\share2 is not inside the host directory of --mount /data\r\n"}]}, {"name":"validation: allow outside denied path","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount-allow","{root}\\share\\hidden\\shown"],"status":1,"stdout":"","stderr":"error: --mount-allow exposes a path inside a --mount-deny path of the same share, which has none\r\n"}]}, {"name":"validation: write in read-only share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,ro","--mount-write","{root}\\share\\writable"],"status":1,"stdout":"","stderr":"error: --mount-write narrows the writes of a read-write share; --mount /data is read-only\r\n"}]}, {"name":"validation: reserved command-line token","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--cmdline","nvx_sandbox=0"],"status":1,"stdout":"","stderr":"error: nvx_sandbox is owned by the sandbox profile\r\n"}]}, @@ -464,10 +464,10 @@ {"name":"run: missing kernel","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"remove":["build/vmlinux"],"status":1,"stdout":"","stderr":"error: Linux direct kernel not found: {root}\\build\\vmlinux\r\n"}]}, {"name":"run: missing initramfs","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"remove":["build/initramfs.cpio.gz"],"status":1,"stdout":"","stderr":"error: initramfs not found: {root}\\build\\initramfs.cpio.gz\r\n"}]}, {"name":"lifecycle: minimal","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":"hello world\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/tmp"],"status":0,"stdout":"/tmp\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/sh","--arg=-c","--arg","echo out; echoerr err; exit 3","--outcome-report","{root}\\reports\\exec.json"],"check":["reports/exec.json"],"status":3,"stdout":"out\n","stderr":"err\n","files":{"reports/exec.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"exit\",\n \"operation\": \"exec\",\n \"status_code\": 3\n },\n \"schema_version\": 1\n}\n"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/env","--environment","A=1","--environment","B=two"],"status":0,"stdout":"A=1\nB=two\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/printenv","--arg","PATH","--inherit-default-environment","--environment","A=1"],"status":0,"stdout":"/usr/sbin:/usr/bin:/sbin:/bin\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/env","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"X=1\", \"Y=two words\"]"},"status":0,"stdout":"X=1\nY=two words\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/env","--environment-file","{root}\\empty.json"],"write":{"empty.json":"[]"},"status":0,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/sh","--arg=-c","--arg","sleep 5000","--exec-timeout-ms","50","--outcome-report","{root}\\reports\\exec-timeout.json"],"check":["reports/exec-timeout.json"],"status":124,"stdout":"","stderr":"","files":{"reports/exec-timeout.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"timeout\",\n \"operation\": \"exec\",\n \"status_code\": 124\n },\n \"schema_version\": 1\n}\n"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/sh","--arg=-c","--arg","signal 9"],"status":137,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/missing"],"status":127,"stdout":"","stderr":"openvmm: /bin/missing: not found\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/missing"],"status":1,"stdout":"","stderr":"error: managed guest rejected exec (status=2, category=cwd-failed)\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","relative"],"status":1,"stdout":"","stderr":"error: managed exec working directory must be an absolute path of at most 4096 bytes\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/echo","--outcome-report","{root}\\reports\\exec.json"],"status":1,"stdout":"","stderr":"error: outcome report already exists: {root}\\reports\\exec.json\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/echo","--outcome-report","{root}\\missing\\exec.json"],"status":1,"stdout":"","stderr":"error: outcome report parent is not a plain directory: {root}\\missing\r\n"},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: one share","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 2,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 2,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root_json}\\\\share,ro\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/data"],"status":0,"stdout":"/data\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 2,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 2,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 2,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: several shares","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount","/other,{root}\\share2,rw"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 4,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 4,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-bd47413b5c03dfc6e4b5b8143fe205ea,{root_json}\\\\share,ro\",\"--mount-child\",\"1-bf2faee2596026620b5cbc997c2d7d45,{root_json}\\\\share2,rw\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,ro nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,rw\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/other"],"status":0,"stdout":"/other\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 4,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 4,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 4,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: share policies","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rw","--mount-deny","{root}\\share\\hidden","--mount-allow","{root}\\share\\hidden\\shown","--mount-write","{root}\\share\\writable"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 5,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 5,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root_json}\\\\share,rw\",\"--mount-deny\",\"{root_json}\\\\share\\\\hidden\",\"--mount-allow\",\"{root_json}\\\\share\\\\hidden\\\\shown\",\"--mount-write\",\"{root_json}\\\\share\\\\writable\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 5,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 5,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 5,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, -{"name":"lifecycle: file share","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/etc/single.txt,{root}\\single.txt,ro"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 6,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 6,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root_json}\\\\single.txt,ro,file\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 6,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 6,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 6,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: one share","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root_json}\\\\share,ro\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/data"],"status":0,"stdout":"/data\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: several shares","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount","/other,{root}\\share2,rw"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n },\r\n {\r\n \"child\": 1,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/other\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n },\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share2\",\r\n \"writable\": true,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n },\r\n {\r\n \"child\": 1,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/other\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n },\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share2\",\r\n \"writable\": true,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-bd47413b5c03dfc6e4b5b8143fe205ea,{root_json}\\\\share,ro\",\"--mount-child\",\"1-bf2faee2596026620b5cbc997c2d7d45,{root_json}\\\\share2,rw\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,ro nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,rw\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/other"],"status":0,"stdout":"/other\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n },\r\n {\r\n \"child\": 1,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/other\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n },\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share2\",\r\n \"writable\": true,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n },\r\n {\r\n \"child\": 1,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/other\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n },\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share2\",\r\n \"writable\": true,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n },\r\n {\r\n \"child\": 1,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/other\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": false,\r\n \"write\": []\r\n },\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"root\": \"{root_json}\\\\share2\",\r\n \"writable\": true,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: share policies","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rw","--mount-deny","{root}\\share\\hidden","--mount-allow","{root}\\share\\hidden\\shown","--mount-write","{root}\\share\\writable"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"deniedIdentities\": [\r\n null\r\n ],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": true,\r\n \"write\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"deniedIdentities\": [\r\n null\r\n ],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": true,\r\n \"write\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root_json}\\\\share,rw\",\"--mount-deny\",\"{root_json}\\\\share\\\\hidden\",\"--mount-allow\",\"{root_json}\\\\share\\\\hidden\\\\shown\",\"--mount-write\",\"{root_json}\\\\share\\\\writable\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"deniedIdentities\": [\r\n null\r\n ],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": true,\r\n \"write\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"deniedIdentities\": [\r\n null\r\n ],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": true,\r\n \"write\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": false,\r\n \"source\": \"\",\r\n \"target\": \"/data\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"deniedIdentities\": [\r\n null\r\n ],\r\n \"root\": \"{root_json}\\\\share\",\r\n \"writable\": true,\r\n \"write\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: file share","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/etc/single.txt,{root}\\single.txt,ro"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/etc/single.txt\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"file\": true,\r\n \"root\": \"{root_json}\\\\single.txt\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/etc/single.txt\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"file\": true,\r\n \"root\": \"{root_json}\\\\single.txt\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root_json}\\\\single.txt,ro,file\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/etc/single.txt\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"file\": true,\r\n \"root\": \"{root_json}\\\\single.txt\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/etc/single.txt\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"file\": true,\r\n \"root\": \"{root_json}\\\\single.txt\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"export\": {\r\n \"binds\": [\r\n {\r\n \"child\": 0,\r\n \"readOnly\": true,\r\n \"source\": \"\",\r\n \"target\": \"/etc/single.txt\"\r\n }\r\n ],\r\n \"children\": [\r\n {\r\n \"allowed\": [],\r\n \"denied\": [],\r\n \"deniedIdentities\": [],\r\n \"file\": true,\r\n \"root\": \"{root_json}\\\\single.txt\",\r\n \"writable\": false,\r\n \"write\": []\r\n }\r\n ]\r\n },\r\n \"format\": 7,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, {"name":"lifecycle: network and launch options","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/server","--arg=--serve","--hostname","worker","--workload-user","1000:1000","--memory-max","268435456","--pids-max","64","--memory-mib","384","--hypervisor","auto","--cmdline","quiet","--net","10.0.0.2/24","--network-profile","portable","--network-egress","allow","--network-ingress","deny","--network-egress-policy-file","{root}\\policy.json","--host-loopback","allow","--host-loopback-forward","tcp:18080:8080","--host-loopback-forward","udp:19090:9090"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 443}], \"deny\": [{\"cidr\": \"192.0.2.128/25\"}]}"},"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"network\": {\r\n \"allow\": {\r\n \"name\": \"--network-egress-allow\",\r\n \"rules\": [\r\n {\r\n \"ports\": [\r\n {\r\n \"endPort\": null,\r\n \"port\": 443,\r\n \"protocol\": \"tcp\"\r\n }\r\n ],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.0/24\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"deny\": {\r\n \"name\": \"--network-egress-deny\",\r\n \"rules\": [\r\n {\r\n \"ports\": [],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.128/25\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"egress\": \"allow\",\r\n \"guestNetwork\": \"10.0.0.2/24\",\r\n \"hostLoopback\": \"allow\",\r\n \"hostLoopbackForwards\": [\r\n {\r\n \"guestPort\": 8080,\r\n \"hostPort\": 18080,\r\n \"protocol\": \"tcp\"\r\n },\r\n {\r\n \"guestPort\": 9090,\r\n \"hostPort\": 19090,\r\n \"protocol\": \"udp\"\r\n }\r\n ],\r\n \"ingress\": \"deny\",\r\n \"proxy\": null\r\n },\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"network\": {\r\n \"allow\": {\r\n \"name\": \"--network-egress-allow\",\r\n \"rules\": [\r\n {\r\n \"ports\": [\r\n {\r\n \"endPort\": null,\r\n \"port\": 443,\r\n \"protocol\": \"tcp\"\r\n }\r\n ],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.0/24\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"deny\": {\r\n \"name\": \"--network-egress-deny\",\r\n \"rules\": [\r\n {\r\n \"ports\": [],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.128/25\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"egress\": \"allow\",\r\n \"guestNetwork\": \"10.0.0.2/24\",\r\n \"hostLoopback\": \"allow\",\r\n \"hostLoopbackForwards\": [\r\n {\r\n \"guestPort\": 8080,\r\n \"hostPort\": 18080,\r\n \"protocol\": \"tcp\"\r\n },\r\n {\r\n \"guestPort\": 9090,\r\n \"hostPort\": 19090,\r\n \"protocol\": \"udp\"\r\n }\r\n ],\r\n \"ingress\": \"deny\",\r\n \"proxy\": null\r\n },\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"1000:1000\",\"--net\",\"10.0.0.2/24\",\"--network-profile\",\"portable\",\"--network-egress\",\"allow\",\"--network-ingress\",\"deny\",\"--host-loopback\",\"allow\",\"--network-egress-allow\",\"192.0.2.0/24:tcp:443\",\"--network-egress-deny\",\"192.0.2.128/25\",\"--host-loopback-forward\",\"tcp:18080:8080\",\"--host-loopback-forward\",\"udp:19090:9090\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"384M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=worker nvx_memory_max=268435456 nvx_pids_max=65\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"network\": {\r\n \"allow\": {\r\n \"name\": \"--network-egress-allow\",\r\n \"rules\": [\r\n {\r\n \"ports\": [\r\n {\r\n \"endPort\": null,\r\n \"port\": 443,\r\n \"protocol\": \"tcp\"\r\n }\r\n ],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.0/24\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"deny\": {\r\n \"name\": \"--network-egress-deny\",\r\n \"rules\": [\r\n {\r\n \"ports\": [],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.128/25\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"egress\": \"allow\",\r\n \"guestNetwork\": \"10.0.0.2/24\",\r\n \"hostLoopback\": \"allow\",\r\n \"hostLoopbackForwards\": [\r\n {\r\n \"guestPort\": 8080,\r\n \"hostPort\": 18080,\r\n \"protocol\": \"tcp\"\r\n },\r\n {\r\n \"guestPort\": 9090,\r\n \"hostPort\": 19090,\r\n \"protocol\": \"udp\"\r\n }\r\n ],\r\n \"ingress\": \"deny\",\r\n \"proxy\": null\r\n },\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","openvmm.log","runtime.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"network\": {\r\n \"allow\": {\r\n \"name\": \"--network-egress-allow\",\r\n \"rules\": [\r\n {\r\n \"ports\": [\r\n {\r\n \"endPort\": null,\r\n \"port\": 443,\r\n \"protocol\": \"tcp\"\r\n }\r\n ],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.0/24\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"deny\": {\r\n \"name\": \"--network-egress-deny\",\r\n \"rules\": [\r\n {\r\n \"ports\": [],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.128/25\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"egress\": \"allow\",\r\n \"guestNetwork\": \"10.0.0.2/24\",\r\n \"hostLoopback\": \"allow\",\r\n \"hostLoopbackForwards\": [\r\n {\r\n \"guestPort\": 8080,\r\n \"hostPort\": 18080,\r\n \"protocol\": \"tcp\"\r\n },\r\n {\r\n \"guestPort\": 9090,\r\n \"hostPort\": 19090,\r\n \"protocol\": \"udp\"\r\n }\r\n ],\r\n \"ingress\": \"deny\",\r\n \"proxy\": null\r\n },\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":{"text":"{\n \"format\": 4,\n \"launch\": \"{hex32}\",\n \"process\": {\n \"pid\": {int},\n \"startTime\": {int},\n \"boot\": \"{hex32}\"\n },\n \"endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\n \"runtime\": {\n \"openvmm\": \"sha256:1e8c924cdaac227bf74b4a4a9bcdb9e8836a1355fd0408cc81dab5d9aa71eb4d\",\n \"kernel\": \"sha256:a0c936696eb7d5ee3192bf53b9d281cecbb40ca9db520de72cb95817ad92ac72\",\n \"initramfs\": \"sha256:2f77c2c97621e8bf69794e9d12815bb7c46338638f126bea8b18dfe02a684d62\",\n \"bootContract\": 3,\n \"controlContract\": \"nvx-microvm-v2-control-v2\",\n \"controlSession\": 1\n }\n}\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","openvmm.log","outcome.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"network\": {\r\n \"allow\": {\r\n \"name\": \"--network-egress-allow\",\r\n \"rules\": [\r\n {\r\n \"ports\": [\r\n {\r\n \"endPort\": null,\r\n \"port\": 443,\r\n \"protocol\": \"tcp\"\r\n }\r\n ],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.0/24\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"deny\": {\r\n \"name\": \"--network-egress-deny\",\r\n \"rules\": [\r\n {\r\n \"ports\": [],\r\n \"to\": [\r\n {\r\n \"cidr\": \"192.0.2.128/25\",\r\n \"except\": []\r\n }\r\n ]\r\n }\r\n ]\r\n },\r\n \"egress\": \"allow\",\r\n \"guestNetwork\": \"10.0.0.2/24\",\r\n \"hostLoopback\": \"allow\",\r\n \"hostLoopbackForwards\": [\r\n {\r\n \"guestPort\": 8080,\r\n \"hostPort\": 18080,\r\n \"protocol\": \"tcp\"\r\n },\r\n {\r\n \"guestPort\": 9090,\r\n \"hostPort\": 19090,\r\n \"protocol\": \"udp\"\r\n }\r\n ],\r\n \"ingress\": \"deny\",\r\n \"proxy\": null\r\n },\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, {"name":"lifecycle: proxy-only network","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--host-loopback","deny","--network-proxy","10.0.0.1:3128"],"check":["state/config.json"],"status":0,"stdout":"","stderr":"","files":{"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": {\r\n \"allow\": {\r\n \"name\": \"--network-egress-allow\",\r\n \"rules\": []\r\n },\r\n \"deny\": {\r\n \"name\": \"--network-egress-deny\",\r\n \"rules\": []\r\n },\r\n \"egress\": \"deny\",\r\n \"guestNetwork\": \"10.0.0.2/24\",\r\n \"hostLoopback\": \"deny\",\r\n \"hostLoopbackForwards\": [],\r\n \"ingress\": \"deny\",\r\n \"proxy\": {\r\n \"port\": 3128,\r\n \"scheme\": null\r\n }\r\n },\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:\\\\\\\\?\\\\{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:\\\\\\\\?\\\\{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--net\",\"10.0.0.2/24\",\"--network-profile\",\"portable\",\"--network-egress\",\"deny\",\"--network-ingress\",\"deny\",\"--host-loopback\",\"deny\",\"--network-proxy\",\"10.0.0.1:3128\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs:11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, {"name":"state: operations without a sandbox","steps":[{"argv":["sandbox","start","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"}]}, @@ -475,7 +475,7 @@ {"name":"state: relative and nested state directory","steps":[{"argv":["sandbox","provision","--state-dir","nested/state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"check":["nested/state","nested/state/config.json"],"status":0,"stdout":"","stderr":"","files":{"nested/state":{"entries":["config.json","sandbox.lock"]},"nested/state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"}}},{"argv":["sandbox","deprovision","--state-dir","nested/state"],"check":["nested/state"],"status":0,"stdout":"","stderr":"","files":{"nested/state":null}}]}, {"name":"state: failed start","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--cmdline","fake_exit_on_start=5"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\r\n","files":{"state":{"entries":["config.json","openvmm.log","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"fake_exit_on_start=5\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\n"}}},{"argv":["sandbox","start","--state-dir","{root}\\state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\r\n","files":{"state":{"entries":["config.json","openvmm.log","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"fake_exit_on_start=5\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\n"}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, {"name":"state: start without OpenVMM","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state"],"remove":["openvmm/target/release/openvmm{exe}"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM release binary not found: {root}\\openvmm\\target\\release\\openvmm.exe\r\n","files":{"state":{"entries":["config.json","sandbox.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"distro_binding\": {\r\n \"format\": \"erofs\",\r\n \"identity\": \"11111111-1111-1111-1111-111111111111\",\r\n \"image\": {\r\n \"changed\": null,\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 7,\r\n \"modified\": {\r\n \"nanoseconds\": {int},\r\n \"seconds\": {int}\r\n },\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"manifest\": null,\r\n \"policy\": {\r\n \"result\": \"accepted\",\r\n \"revision\": 1\r\n },\r\n \"version\": 1\r\n },\r\n \"format\": 1,\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"network\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"scratch_binding\": {\r\n \"device\": {int},\r\n \"fileId\": \"{hex32}\",\r\n \"kind\": \"regular\",\r\n \"length\": 8,\r\n \"scheme\": \"windows\",\r\n \"version\": 1\r\n },\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"status":0,"stdout":"","stderr":""}]}, -{"name":"state: malformed configuration","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"{\"format\": 7}"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}\\state\\config.json\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"[]"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}\\state\\config.json\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"{\"format\": 1}"},"status":1,"stdout":"","stderr":"error: sandbox configuration is malformed\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, +{"name":"state: malformed configuration","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"{\"format\": 8}"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}\\state\\config.json\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"[]"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}\\state\\config.json\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"{\"format\": 1}"},"status":1,"stdout":"","stderr":"error: sandbox configuration is malformed\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, {"name":"state: not a directory","steps":[{"argv":["sandbox","start","--state-dir","{root}\\single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\single.txt\r\n"},{"argv":["sandbox","provision","--state-dir","{root}\\single.txt","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: [WinError 183] Cannot create a file when that file already exists: '{root_json}\\\\single.txt'\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\single.txt\r\n"}]}, {"name":"state: foreign file","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"write":{"state/foreign.txt":"x"},"check":["state"],"status":1,"stdout":"","stderr":"error: sandbox state directory contains files not owned by NVX: foreign.txt\r\n","files":{"state":{"entries":["foreign.txt"]}}}]}, {"name":"run, usage: help","steps":[{"argv":["run","-h"],"status":0,"stdout":"usage: nvx-rs run [-h] [--guest {alpine,ubuntu,azurelinux}]\r\n [--hypervisor {auto,whp,kvm,mshv}] [--machine {microvm}]\r\n [--memory-mib MEMORY_MIB]\r\n [--memory-capacity-mib MEMORY_CAPACITY_MIB]\r\n [--processors {1,2,4,8}] [--cpu-profile ID]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny MOUNT_DENY] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--restore-snapshot RESTORE_SNAPSHOT]\r\n [--restore-processors {1,2,4,8}]\r\n [--restore-memory-mib RESTORE_MEMORY_MIB]\r\n [--restore-ready-path RESTORE_READY_PATH] [--dry-run]\r\n\r\noptions:\r\n -h, --help show this help message and exit\r\n --guest {alpine,ubuntu,azurelinux}\r\n --hypervisor {auto,whp,kvm,mshv}\r\n --machine {microvm}\r\n --memory-mib MEMORY_MIB\r\n guest RAM in MiB (default by --guest: alpine 128,\r\n ubuntu 512, azurelinux 512)\r\n --memory-capacity-mib MEMORY_CAPACITY_MIB\r\n --processors {1,2,4,8}\r\n --cpu-profile ID OpenVMM CPU profile: auto (OpenVMM's default) for the\r\n built-in profile of the host's CPU, a built-in profile\r\n ID, or host to derive a development profile from this\r\n host (doc/usage.md)\r\n --mount GUEST_TARGET,HOST_PATH[,ro|rw]\r\n live-share a host directory, or a single regular host\r\n file without its directory; the mode follows the last\r\n comma, so HOST_PATH may contain commas when the mode\r\n is given; repeat for more shares, each with its own\r\n guest target and mode, as children of one virtio-fs\r\n device\r\n --mount-deny MOUNT_DENY\r\n --mount-allow HOST_PATH\r\n expose one existing path inside a --mount-deny path\r\n again\r\n --mount-write HOST_PATH\r\n make one existing path one of the only writable parts\r\n of a read-write --mount\r\n --mount-owner {vmm,caller}\r\n host identity of the share's file operations: vmm\r\n (default) or caller, with guest root squashed to the\r\n owner of the shared directory or file (Linux only)\r\n --net IPV4/PREFIX\r\n --network-profile {portable}\r\n --network-egress {allow,deny}\r\n --network-ingress {allow,deny}\r\n --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]\r\n allow guest egress to an IPv4 or IPv6 CIDR, optionally\r\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\r\n udp only on PORT or on the inclusive range PORT-PORT;\r\n repeat to add rules\r\n --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]\r\n deny guest egress to an IPv4 or IPv6 CIDR, optionally\r\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\r\n udp only on PORT or on the inclusive range PORT-PORT;\r\n repeat to add rules\r\n --network-egress-policy-file PATH\r\n load bounded MXC-shaped IPv4 and IPv6 egress rules\r\n from JSON\r\n --host-loopback {allow,deny}\r\n --network-proxy IPV4:TCP-PORT\r\n --host-loopback-forward HOST_LOOPBACK_FORWARD\r\n --outcome-report OUTCOME_REPORT\r\n write a bounded local JSON outcome report\r\n --cmdline CMDLINE\r\n --restore-snapshot RESTORE_SNAPSHOT\r\n --restore-processors {1,2,4,8}\r\n --restore-memory-mib RESTORE_MEMORY_MIB\r\n --restore-ready-path RESTORE_READY_PATH\r\n --dry-run\r\n","stderr":""}]}, diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index 708085e7b..002f5ef17 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -2482,12 +2482,20 @@ def nvx_py(*arguments: str) -> subprocess.CompletedProcess[bytes]: self.assertEqual(result.stderr, b"") command = result.stdout.decode("utf-8") self.assertTrue(command.startswith(">> "), command) + # A relative policy path names a path in the share's host directory, + # and OpenVMM receives the path to which it resolves. + hidden = str(root / "share" / "hidden") + quoted = ( + subprocess.list2cmdline([hidden]) + if os.name == "nt" + else shlex.quote(hidden) + ) for expected in ( "--microvm-sandbox-block distro:file:distro.erofs,ro", "--microvm-sandbox-block scratch:file:scratch.ext4", "--hypervisor kvm", "nvx_arg=-c nvx_arg=true nvx_memory_max=268435456", - "--mount-deny hidden", + f"--mount-deny {quoted}", ): self.assertIn(expected, command) # A value that nvx-rs refuses is a usage error of nvx.py sandbox.