diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index fdd71a1cc..c09b22c79 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -114,7 +114,7 @@ The adapter's README said what each proved. | Scenario | Fate | Coverage, or why it retires | | --- | --- | --- | | `a_launch_takes_the_bindings_that_the_store_stages_for_its_images` | rewritten | `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_scratch_binding`. The adapter's staging store retires | -| `staging_verifies_the_image_and_binds_the_scratch` | rewritten | `nvx-rs/src/host/sandbox/distro/tests.rs::verification_binds_the_layer_its_manifest_and_the_policy`; `nvx-rs/src/host/sandbox/scratch/tests.rs::binds_refuse_what_is_not_a_scratch_image`; `nvx-rs/tests/security_checklist.rs::a_staged_image_that_was_replaced_fails_its_check` | +| `staging_verifies_the_image_and_binds_the_scratch` | rewritten | `nvx-rs/src/host/sandbox/distro/tests.rs::verification_binds_the_layer_its_manifest_and_the_policy`; `nvx-rs/src/host/sandbox/scratch/tests.rs::binds_refuse_what_is_not_a_scratch_image`; `nvx-rs/src/host/sandbox/distro/tests.rs::a_staged_image_that_was_replaced_fails_its_check` | | `the_host_program_stages_images_in_the_store` | retired | The adapter's `stage` command and its record | | `the_store_records_the_staged_images` | retired | The adapter's record of its staged images | @@ -122,10 +122,10 @@ The adapter's README said what each proved. | Scenario | Fate | Coverage, or why it retires | | --- | --- | --- | -| `a_launch_claims_its_scratch_image_until_it_releases_its_checked_files` | rewritten | `nvx-rs/tests/security_checklist.rs::a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release`; `nvx-rs/src/host/sandbox/scratch/tests.rs::a_claim_lasts_while_the_vmm_holds_the_image` | +| `a_launch_claims_its_scratch_image_until_it_releases_its_checked_files` | rewritten | `nvx-rs/src/host/sandbox/scratch/tests.rs::a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release`; `nvx-rs/src/host/sandbox/scratch/tests.rs::a_claim_lasts_while_the_vmm_holds_the_image` | | `a_launch_reads_only_the_networks_that_the_plan_takes` | rewritten | `nvx-rs/tests/cli.rs::network_policies_that_the_planner_refuses_fail_before_launch`; `nvx-rs/src/host/network/tests.rs::invalid_rules_are_rejected_with_the_policy_compilers_messages` | | `a_launch_records_what_it_launched` | retired | The adapter's launch records. Callers keep `SavedVm`: `nvx-rs/src/host/vm/tests/reattach.rs::verifying_saved_values_checks_runtime_and_process_without_opening_an_endpoint` | -| `a_launch_refuses_images_that_are_not_staged_or_changed_since` | rewritten | `nvx-rs/tests/security_checklist.rs::a_staged_image_that_was_replaced_fails_its_check`; `nvx-rs/src/host/sandbox/scratch/tests.rs::checks_fail_without_reading_when_the_image_changes`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding` | +| `a_launch_refuses_images_that_are_not_staged_or_changed_since` | rewritten | `nvx-rs/src/host/sandbox/distro/tests.rs::a_staged_image_that_was_replaced_fails_its_check`; `nvx-rs/src/host/sandbox/scratch/tests.rs::checks_fail_without_reading_when_the_image_changes`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding` | | `a_launch_refuses_shares_that_it_cannot_export` | rewritten | `nvx-rs/src/host/filesystem/tests.rs::policy_paths_hide_show_and_narrow_writes`; `nvx-rs/tests/security_checklist.rs::an_export_refuses_a_read_only_path_inside_a_writable_directory` | | `a_launch_renders_each_element_of_its_network_through_the_plan` | rewritten | `nvx-rs/src/host/config/tests.rs::cold_boot_arguments_lower_layered_roots_networks_and_exports_by_path`; `nvx-rs/src/host/network/tests.rs::proxy_urls_name_the_gateway_as_the_guest_sees_it` | | `a_launch_shares_host_paths_inside_the_authorized_directories` | rewritten | `nvx-rs/src/host/filesystem/tests.rs::policy_paths_hide_show_and_narrow_writes`; `nvx-rs/src/host/config/tests.rs::cold_boot_arguments_lower_layered_roots_networks_and_exports_by_path` | @@ -151,7 +151,7 @@ The adapter's README said what each proved. | Scenario | Fate | Coverage, or why it retires | | --- | --- | --- | -| `a_launch_that_the_hook_ends_after_its_checks_leaves_no_launch` | rewritten | A launch that fails after its checks leaves nothing of its own: `nvx-rs/src/host/vm/tests.rs::creation_rolls_back_its_log_and_never_removes_preexisting_outputs_or_inputs`; `nvx-rs/tests/security_checklist.rs::a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release`. The step hook retires | +| `a_launch_that_the_hook_ends_after_its_checks_leaves_no_launch` | rewritten | A launch that fails after its checks leaves nothing of its own: `nvx-rs/src/host/vm/tests.rs::creation_rolls_back_its_log_and_never_removes_preexisting_outputs_or_inputs`; `nvx-rs/src/host/sandbox/scratch/tests.rs::a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release`. The step hook retires | | `images_replaced_after_the_checks_never_reach_the_vm` | retired | The checked-file handoff leaves (SL6.3, #1122): a launch names its images by path, so what takes an image's path after its check is what OpenVMM opens. Callers stage their images in a directory that only their own account can write, as the docs of `host::sandbox::distro`, `host::sandbox::scratch` and `host::seal` require, so that no other account can replace a checked image | | `the_hook_pauses_a_launch_at_each_of_its_steps` | retired | The adapter's step hook | @@ -159,13 +159,13 @@ The adapter's README said what each proved. | Scenario | Fate | Coverage, or why it retires | | --- | --- | --- | -| `a_changed_image_is_refused_before_spawn` | rewritten | `nvx-rs/tests/security_checklist.rs::a_staged_image_that_was_replaced_fails_its_check`; `nvx-rs/src/host/sandbox/distro/tests.rs::the_staged_check_fails_without_reading_when_the_layer_changes`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding` | +| `a_changed_image_is_refused_before_spawn` | rewritten | `nvx-rs/src/host/sandbox/distro/tests.rs::a_staged_image_that_was_replaced_fails_its_check`; `nvx-rs/src/host/sandbox/distro/tests.rs::the_staged_check_fails_without_reading_when_the_layer_changes`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding` | | `a_denied_network_lets_no_probe_through` | rewritten | A deny-all network now keeps its NIC, which lets nothing through: `nvx-rs/src/host/config/tests.rs::an_explicit_deny_all_network_keeps_its_nic`; `test-microvm --scenario directional-network-policy` | | `a_failed_mapping_tears_the_vm_down` | rewritten | The guest's refusal, which keeps the mappings before it, latches the table and refuses the start: `nvx-rs/src/guest/mappings/table/tests.rs::a_failed_mapping_keeps_those_before_it_and_latches_the_table`; `nvx-rs/src/guest/managed_agent/dispatch/tests.rs::a_failed_mapping_keeps_refusing_workloads_through_dispatch`. The stop that the guest confirms, with a successful outcome and no record of the VM: `nvx-rs/tests/cli.rs::a_mapping_that_the_guest_refuses_leaves_no_usable_start`. The teardown of the root and its mappings: `nvx-rs/src/guest/root/tests.rs::a_teardown_unmounts_the_layers_top_first_and_reports_every_failure`; `nvx-rs/src/guest/mappings/mapper/tests/privileged.rs::a_teardown_unmounts_each_mapping_and_reports_a_busy_one` | | `a_forward_reaches_its_guest_port` | rewritten | `test-microvm --scenario host-loopback-policy` | | `a_proxy_only_network_lets_only_its_proxy_through` | rewritten | `test-microvm --scenario sandbox-proxy-only` | | `a_read_only_share_inside_a_writable_one_is_refused_before_launch` | rewritten | `nvx-rs/tests/security_checklist.rs::an_export_refuses_a_read_only_path_inside_a_writable_directory` | -| `a_replaced_scratch_is_refused_before_spawn` | rewritten | `nvx-rs/tests/security_checklist.rs::a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_scratch_binding` | +| `a_replaced_scratch_is_refused_before_spawn` | rewritten | `nvx-rs/src/host/sandbox/scratch/tests.rs::a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release`; `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_scratch_binding` | | `a_share_inside_a_reserved_tree_is_refused_before_launch` | rewritten | `nvx-rs/tests/security_checklist.rs::an_export_refuses_guest_targets_that_are_not_canonical_or_that_a_root_reserves`; `nvx-rs/src/host/filesystem/tests.rs::reserved_targets_depend_on_the_root` | | `a_share_over_a_reserved_tree_is_refused_before_launch` | rewritten | `nvx-rs/tests/security_checklist.rs::an_export_refuses_guest_targets_that_are_not_canonical_or_that_a_root_reserves`; `nvx-rs/src/host/filesystem/tests.rs::reserved_targets_depend_on_the_root` | | `accountless_workload_identity_fails_its_stage` | rewritten | `test-microvm --scenario workload-identity`; `nvx-rs/src/guest/init/boot/tests.rs::account_creation_replaces_only_the_requested_unusable_identity_once` | @@ -201,7 +201,7 @@ The adapter's README said what each proved. | `published_listeners_refuse_binding_and_raw_sockets` | retired | The capability allowlist of the old guest entry leaves with the exec engine (#1087): nvx-init runs each execution as a plain process | | `recovery_required_layer_fails_the_assembly` | rewritten | `nvx-rs/src/guest/root/tests.rs::a_layer_that_is_not_ext4_or_whose_journal_needs_a_recovery_is_refused_before_any_mount` | | `root_profiles_enter_the_root` | retired | Per-execution namespaces, root profiles and capability sets leave with the exec engine (#1087) | -| `unstaged_images_are_refused_before_spawn` | rewritten | `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding`; `nvx-rs/tests/security_checklist.rs::a_staged_image_that_was_replaced_fails_its_check` | +| `unstaged_images_are_refused_before_spawn` | rewritten | `nvx-rs/tests/cli.rs::start_refuses_a_configuration_without_a_valid_distro_binding`; `nvx-rs/src/host/sandbox/distro/tests.rs::a_staged_image_that_was_replaced_fails_its_check` | ### `vm_template` (8) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index abc8e4856..5cb11ebd5 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -116,16 +116,14 @@ changes only when the module's purpose does. | [`nvx::host::snapshot`](#nvxhostsnapshot) | Strict private snapshots, post-exit capture finishing, verified metadata, VM compatibility, captured NIC addresses, and time eligibility | | [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity | | [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener | -| [`nvx::host::artifacts`](#nvxhostartifacts) | Locating the OpenVMM executable and the guest artifacts that a VM runs, and their runtime identity | | [`nvx::host::network`](#nvxhostnetwork) | The one lowering of a network posture into OpenVMM options, for the CLI and external orchestrators | | [`nvx::host::network::policy`](#nvxhostnetworkpolicy) | Egress policy files and their compilation into OpenVMM's native rules | | [`nvx::host::filesystem`](#nvxhostfilesystem) | The planner of the host paths that every NVX caller exports to the guest through OpenVMM's virtio-fs device | | [`nvx::host::nvxs`](#nvxhostnvxs) | Compatibility paths for the host NVXS connection and its retained cause details | | [`nvx::host::transport`](#nvxhosttransport) | A verified VM endpoint connection and the blocking host NVXS driver, with the existing timed transport | | [`nvx::host::process`](#nvxhostprocess) | Verified references to OpenVMM processes, through which callers wait for them and end them | -| [`nvx::host::seal`](#nvxhostseal) | `FileSeal`: what the host reports about an open file from its metadata alone, without reading it | | [`nvx::host::platform`, `report`](#nvxhostplatform-report) | Linux and Windows primitives, and OpenVMM's outcome report | -| [`nvx::host::sandbox`](#nvxhostsandbox) | The launch profile of `nvx-rs sandbox`: its layer, live shares, option checks, and bindings | +| [`nvx::host::sandbox`](#nvxhostsandbox) | The launch profile of `nvx-rs sandbox`: its live shares and option checks; its layer and bindings are private to the CLI | | [`nvx::guest`](#nvxguest) | The programs that run in Linux guests; see [Guest binaries](#guest-binaries) | | [`nvx::guest::mappings`](#nvxguestmappings) | `Mapping` and `MappingTable`: host mappings, and the account of a host's mapping table, for any guest init or agent | | [`nvx::guest::nvxs`](#nvxguestnvxs) | `GuestConnection`: the guest's role of the NVXS session over a byte stream, such as the control console | @@ -921,19 +919,6 @@ namespaces, it uses passwordless `sudo` only for that isolated helper, not the test suite or build. If neither route works, the test fails; it never changes host namespace policy or skips the identity assertions. -### `nvx::host::artifacts` - -`ArtifactDir` and `Guest`: every artifact of the guests that `nvx.py` runs, the -OpenVMM executable, the kernel, the Alpine, Ubuntu, and Azure Linux initramfs -images and their package manifests, and the Ubuntu distro layer and its -manifest, in a checkout's `build/` and `openvmm/target/release/` or a release's -`guest/` and `bin/`, checked with the messages of `nvx.py`. `ArtifactPaths` -lists them all and serializes as JSON. - -`ArtifactDir::runtime_identity` reads the `RuntimeIdentity` of the OpenVMM -executable, kernel, and Alpine initramfs from the install metadata, a release's -`SHA256SUMS` or a checkout's build provenance, without hashing an artifact. - ### `nvx::host::network` `NetworkPolicy` and `openvmm_arguments`, the one lowering of a network posture @@ -1046,14 +1031,6 @@ holds what the process's children read once it reaped them, as the kernel accounts them to their parent, while Windows counts the process's own reads alone. -### `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::platform`, `report` Linux and Windows primitives, and OpenVMM's outcome report. The authenticated @@ -1086,8 +1063,16 @@ it, and NVX does not depend on an asynchronous I/O runtime. ### `nvx::host::sandbox` The launch profile of `nvx-rs sandbox`, with native input validation: the -distro `SandboxLayer`, an EROFS layer or a GPT disk, the one layer that a -sandbox takes, and the workload identity that `--workload-user` names. +workload identity that `--workload-user` names, the live shares, and the checks +of the options. The image staging of the profile is private to the crate, for +the CLI (feature `cli`): `layer`'s `SandboxLayer`, the distro layer of a +sandbox, an EROFS layer or a GPT disk, the one layer that a sandbox takes; +`distro`'s `DistroBinding` of the layer, which explicit verification makes, and +its check from metadata alone; `scratch`'s `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 the seals of `nvx::host`'s private +`seal`, which they record. See [Distro bindings](#distro-bindings) and +[Images by path](#images-by-path). `mount`: `SandboxMount` requests of live shares of host directories and files, their guest targets and the reserved ones, their access policies and owner, and @@ -1100,14 +1085,6 @@ to shares, the systemd entrypoint check and the explicit verification of the distro layer, the targets of `nvx-rs run` shares, and the managed exec environment; see [Sandbox input contract](#sandbox-input-contract). -`distro`: the `DistroBinding` of a distro layer, an EROFS layer or a GPT disk, -which explicit verification makes, 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. - ### `nvx::guest` The programs that run in Linux guests; see [Guest binaries](#guest-binaries). @@ -1661,7 +1638,8 @@ file, that each refusal names, a fragment of its reason, and its error code. ## Distro bindings Three separate checks guard the distro layer of a sandbox, and only explicit -verification reads it: +verification reads it. The bindings and the seals that they record are private +to the crate, for the CLI: - `options::validate_sandbox_entrypoint` refuses the entrypoints of systemd, which the sandbox security profile does not run. Every launch applies it, @@ -1730,9 +1708,9 @@ scratch image at the paths that the VM's configuration holds. Their checks, unchanged from its metadata alone and return it open, but what takes an image's path after its check is what OpenVMM opens. Callers therefore stage their images in a directory that only their own account can write, as -[Distro bindings](#distro-bindings) and the docs of `nvx::host::sandbox::distro`, -`nvx::host::sandbox::scratch`, and `nvx::host::seal` require, so that no other -account can replace a checked image. On Windows, a checked distro layer also +[Distro bindings](#distro-bindings) and the docs of the crate's private +`host::sandbox::distro`, `host::sandbox::scratch`, and `host::seal` require, so +that no other account can replace a checked image. On Windows, a checked distro layer also denies other opens for writing or deleting the layer while it is held, and the scratch image's guard denies deleting and renaming the image. `nvx-rs sandbox start` and `run` check the images just before they create the VM, and hold the @@ -2823,14 +2801,16 @@ python3 ../scripts/nvx.py test-guest-binaries \ The [security checklist](tests/security_checklist.rs) checks, through the public API alone and without a VM, each security requirement that the host's documentation states: the guest targets of an export and a read-only path -inside a writable one, the checks of a staged image and the claim of a scratch -image, which callers keep in directories that only their own account can write, -the exported objects that each start checks again, a VM's private directory and -its new private log, the exit proof of a saved VM, the owner of its endpoint, -and its recovery from the saved values, and a snapshot's strict directory. An -idle copy of the test binary stands in for a VM's process. The checks that NVX -makes of a restore need a captured snapshot, so the unit tests of -`nvx::host::vm`, `nvx::host::snapshot`, and `nvx::host::restore` check them. +inside a writable one, the exported objects that each start checks again, a +VM's private directory and its new private log, the exit proof of a saved VM, +the owner of its endpoint, and its recovery from the saved values, and a +snapshot's strict directory. An idle copy of the test binary stands in for a +VM's process. The checks that NVX makes of a restore need a captured snapshot, +so the unit tests of `nvx::host::vm`, `nvx::host::snapshot`, and +`nvx::host::restore` check them. The checks of a staged image and the claim of +a scratch image, which the CLI keeps in directories that only its own account +can write, are private to the crate, so the unit tests of its distro and +scratch bindings check them. Two examples compile the documented workflows through the public API alone, and nothing runs them: [`host-workflows`](examples/host-workflows.rs), a cold diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index 9074024e4..274efe131 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -42,7 +42,7 @@ use nvx::host::network::{Access, NetworkPolicy}; use nvx::host::process::{BootId, Opened, Process, ProcessRef}; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; use nvx::host::transport::{self, Transport}; -use nvx::host::{artifacts, report, snapshot, vm}; +use nvx::host::{report, snapshot, vm}; use nvx::protocol::root::RootImage; /// One byte past OpenVMM's 25-byte restore-ready event, so that a longer event fails to parse. @@ -75,9 +75,9 @@ fn main() -> Result<(), Box> { .map_err(Box::::from) }; let runtime = Runtime { - openvmm: artifacts::absolute(Path::new(&args[2]))?, - kernel: artifacts::absolute(Path::new(&args[3]))?, - initramfs: artifacts::absolute(Path::new(&args[4]))?, + openvmm: std::path::absolute(Path::new(&args[2]))?, + kernel: std::path::absolute(Path::new(&args[3]))?, + initramfs: std::path::absolute(Path::new(&args[4]))?, identity: RuntimeIdentity::new(identity(6)?, identity(7)?, identity(8)?), hypervisor, cpu_profile: Some("auto".to_owned()), @@ -121,7 +121,7 @@ fn main() -> Result<(), Box> { }; config.memory_mib = key.memory() / (1 << 20); config.root = RootConfig::Layered(LayeredRoot { - image: artifacts::absolute(Path::new(&args[10]))?, + image: std::path::absolute(Path::new(&args[10]))?, format: RootImage::erofs(None), gpt_first_partition_bound: None, upper: UpperConfig::Ram { diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index bf87e14ef..9a2af9165 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -1591,62 +1591,6 @@ const nvx::guest::view::RESOLVER_FILE (hidden) fn nvx::guest::view::apply (hidden) fn nvx::guest_path mod nvx::host -mod nvx::host::artifacts -struct nvx::host::artifacts::ArtifactDir -method nvx::host::artifacts::ArtifactDir::artifact -method nvx::host::artifacts::ArtifactDir::distro_layer -method nvx::host::artifacts::ArtifactDir::distro_manifest -method nvx::host::artifacts::ArtifactDir::from_dir -method nvx::host::artifacts::ArtifactDir::initramfs -method nvx::host::artifacts::ArtifactDir::kernel -method nvx::host::artifacts::ArtifactDir::layout -method nvx::host::artifacts::ArtifactDir::openvmm -method nvx::host::artifacts::ArtifactDir::package_manifest -method nvx::host::artifacts::ArtifactDir::paths -method nvx::host::artifacts::ArtifactDir::release -method nvx::host::artifacts::ArtifactDir::repository -method nvx::host::artifacts::ArtifactDir::require_initramfs -method nvx::host::artifacts::ArtifactDir::require_kernel -method nvx::host::artifacts::ArtifactDir::root -method nvx::host::artifacts::ArtifactDir::runtime_identity -struct nvx::host::artifacts::ArtifactPaths -field nvx::host::artifacts::ArtifactPaths::guests -field nvx::host::artifacts::ArtifactPaths::kernel -field nvx::host::artifacts::ArtifactPaths::layout -field nvx::host::artifacts::ArtifactPaths::openvmm -field nvx::host::artifacts::ArtifactPaths::root -const nvx::host::artifacts::CHECKSUMS_NAME -const nvx::host::artifacts::DISTRO_MANIFEST_SUFFIX -enum nvx::host::artifacts::Guest -const nvx::host::artifacts::Guest::ALL -variant nvx::host::artifacts::Guest::Alpine -variant nvx::host::artifacts::Guest::AzureLinux -variant nvx::host::artifacts::Guest::Ubuntu -method nvx::host::artifacts::Guest::default_memory_mib -method nvx::host::artifacts::Guest::distribution -method nvx::host::artifacts::Guest::distro_layer_name -method nvx::host::artifacts::Guest::distro_manifest_name -method nvx::host::artifacts::Guest::initramfs_name -method nvx::host::artifacts::Guest::name -method nvx::host::artifacts::Guest::package_manifest_name -struct nvx::host::artifacts::GuestPaths -field nvx::host::artifacts::GuestPaths::distribution -field nvx::host::artifacts::GuestPaths::distro_layer -field nvx::host::artifacts::GuestPaths::distro_manifest -field nvx::host::artifacts::GuestPaths::initramfs -field nvx::host::artifacts::GuestPaths::name -field nvx::host::artifacts::GuestPaths::package_manifest -const nvx::host::artifacts::INITRAMFS_PROVENANCE_NAME -const nvx::host::artifacts::KERNEL_NAME -const nvx::host::artifacts::KERNEL_PROVENANCE_NAME -enum nvx::host::artifacts::Layout -variant nvx::host::artifacts::Layout::Release -variant nvx::host::artifacts::Layout::Repository -const nvx::host::artifacts::MANIFEST_NAME -const nvx::host::artifacts::OPENVMM_PROVENANCE_NAME -const nvx::host::artifacts::PACKAGE_MANIFEST_SUFFIX -fn nvx::host::artifacts::absolute -fn nvx::host::artifacts::openvmm_executable mod nvx::host::cli (hidden) fn nvx::host::cli::main (hidden) mod nvx::host::config @@ -2069,11 +2013,6 @@ mod nvx::host::sandbox const nvx::host::sandbox::DEFAULT_ENTRYPOINT const nvx::host::sandbox::DEFAULT_HOSTNAME const nvx::host::sandbox::DEFAULT_WORKLOAD_IDENTITY -const nvx::host::sandbox::LAYER_ROLES -enum nvx::host::sandbox::LayerRole -variant nvx::host::sandbox::LayerRole::Distro -method nvx::host::sandbox::LayerRole::as_str -method nvx::host::sandbox::LayerRole::from_name enum nvx::host::sandbox::MountAccess variant nvx::host::sandbox::MountAccess::ReadOnly variant nvx::host::sandbox::MountAccess::ReadWrite @@ -2101,14 +2040,6 @@ variant nvx::host::sandbox::PolicyKind::Denied variant nvx::host::sandbox::PolicyKind::Writable method nvx::host::sandbox::PolicyKind::as_str method nvx::host::sandbox::PolicyKind::option -struct nvx::host::sandbox::SandboxLayer -method nvx::host::sandbox::SandboxLayer::format -method nvx::host::sandbox::SandboxLayer::gpt -method nvx::host::sandbox::SandboxLayer::new -method nvx::host::sandbox::SandboxLayer::parse -method nvx::host::sandbox::SandboxLayer::path -method nvx::host::sandbox::SandboxLayer::role -method nvx::host::sandbox::SandboxLayer::uuid struct nvx::host::sandbox::SandboxMount method nvx::host::sandbox::SandboxMount::absolute method nvx::host::sandbox::SandboxMount::absolute_policy_paths @@ -2122,38 +2053,6 @@ method nvx::host::sandbox::SandboxMount::parse method nvx::host::sandbox::SandboxMount::policy method nvx::host::sandbox::SandboxMount::validated method nvx::host::sandbox::SandboxMount::with_kind -mod nvx::host::sandbox::distro -const nvx::host::sandbox::distro::BINDING_VERSION -struct nvx::host::sandbox::distro::CheckedDistro -const nvx::host::sandbox::distro::DISTRO_POLICY_REVISION -struct nvx::host::sandbox::distro::DistroBinding -method nvx::host::sandbox::distro::DistroBinding::check -method nvx::host::sandbox::distro::DistroBinding::verify -enum nvx::host::sandbox::distro::DistroFormat -variant nvx::host::sandbox::distro::DistroFormat::Erofs -variant nvx::host::sandbox::distro::DistroFormat::Gpt -method nvx::host::sandbox::distro::DistroFormat::as_str -enum nvx::host::sandbox::distro::PolicyResult -variant nvx::host::sandbox::distro::PolicyResult::Accepted -variant nvx::host::sandbox::distro::PolicyResult::Rejected -struct nvx::host::sandbox::distro::PolicyVerdict -const nvx::host::sandbox::distro::PolicyVerdict::ACCEPTED -field nvx::host::sandbox::distro::PolicyVerdict::result -field nvx::host::sandbox::distro::PolicyVerdict::revision -mod nvx::host::sandbox::layer -const nvx::host::sandbox::layer::LAYER_ROLES -enum nvx::host::sandbox::layer::LayerRole -variant nvx::host::sandbox::layer::LayerRole::Distro -method nvx::host::sandbox::layer::LayerRole::as_str -method nvx::host::sandbox::layer::LayerRole::from_name -struct nvx::host::sandbox::layer::SandboxLayer -method nvx::host::sandbox::layer::SandboxLayer::format -method nvx::host::sandbox::layer::SandboxLayer::gpt -method nvx::host::sandbox::layer::SandboxLayer::new -method nvx::host::sandbox::layer::SandboxLayer::parse -method nvx::host::sandbox::layer::SandboxLayer::path -method nvx::host::sandbox::layer::SandboxLayer::role -method nvx::host::sandbox::layer::SandboxLayer::uuid mod nvx::host::sandbox::mount const nvx::host::sandbox::mount::AGGREGATE_MOUNT_TARGET const nvx::host::sandbox::mount::MAX_MOUNT_POLICY_PATHS @@ -2253,37 +2152,6 @@ fn nvx::host::sandbox::options::validate_run_share_targets fn nvx::host::sandbox::options::validate_sandbox_entrypoint fn nvx::host::sandbox::parse_workload_identity fn nvx::host::sandbox::require_file -mod nvx::host::sandbox::scratch -const nvx::host::sandbox::scratch::CLAIM_RELEASE_WAIT -struct nvx::host::sandbox::scratch::CheckedScratch -method nvx::host::sandbox::scratch::CheckedScratch::file -const nvx::host::sandbox::scratch::SCRATCH_BINDING_VERSION -struct nvx::host::sandbox::scratch::ScratchBinding -method nvx::host::sandbox::scratch::ScratchBinding::bind -method nvx::host::sandbox::scratch::ScratchBinding::check -mod nvx::host::seal -enum nvx::host::seal::FileKind -variant nvx::host::seal::FileKind::Block -variant nvx::host::seal::FileKind::Character -variant nvx::host::seal::FileKind::Directory -variant nvx::host::seal::FileKind::Fifo -variant nvx::host::seal::FileKind::Other -variant nvx::host::seal::FileKind::Regular -variant nvx::host::seal::FileKind::Socket -struct nvx::host::seal::FileSeal -method nvx::host::seal::FileSeal::device -method nvx::host::seal::FileSeal::file_id -method nvx::host::seal::FileSeal::kind -method nvx::host::seal::FileSeal::length -method nvx::host::seal::FileSeal::of -method nvx::host::seal::FileSeal::scheme -const nvx::host::seal::SEAL_VERSION -enum nvx::host::seal::SealScheme -variant nvx::host::seal::SealScheme::Linux -variant nvx::host::seal::SealScheme::Windows -struct nvx::host::seal::SealTime -field nvx::host::seal::SealTime::nanoseconds -field nvx::host::seal::SealTime::seconds mod nvx::host::snapshot struct nvx::host::snapshot::BlockGeometry field nvx::host::snapshot::BlockGeometry::length diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index c0c08db40..1a4029d05 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -319,62 +319,6 @@ trait nvx::gpt::ReadAt method nvx::gpt::ReadAt::read_exact_at fn nvx::guest_path mod nvx::host -mod nvx::host::artifacts -struct nvx::host::artifacts::ArtifactDir -method nvx::host::artifacts::ArtifactDir::artifact -method nvx::host::artifacts::ArtifactDir::distro_layer -method nvx::host::artifacts::ArtifactDir::distro_manifest -method nvx::host::artifacts::ArtifactDir::from_dir -method nvx::host::artifacts::ArtifactDir::initramfs -method nvx::host::artifacts::ArtifactDir::kernel -method nvx::host::artifacts::ArtifactDir::layout -method nvx::host::artifacts::ArtifactDir::openvmm -method nvx::host::artifacts::ArtifactDir::package_manifest -method nvx::host::artifacts::ArtifactDir::paths -method nvx::host::artifacts::ArtifactDir::release -method nvx::host::artifacts::ArtifactDir::repository -method nvx::host::artifacts::ArtifactDir::require_initramfs -method nvx::host::artifacts::ArtifactDir::require_kernel -method nvx::host::artifacts::ArtifactDir::root -method nvx::host::artifacts::ArtifactDir::runtime_identity -struct nvx::host::artifacts::ArtifactPaths -field nvx::host::artifacts::ArtifactPaths::guests -field nvx::host::artifacts::ArtifactPaths::kernel -field nvx::host::artifacts::ArtifactPaths::layout -field nvx::host::artifacts::ArtifactPaths::openvmm -field nvx::host::artifacts::ArtifactPaths::root -const nvx::host::artifacts::CHECKSUMS_NAME -const nvx::host::artifacts::DISTRO_MANIFEST_SUFFIX -enum nvx::host::artifacts::Guest -const nvx::host::artifacts::Guest::ALL -variant nvx::host::artifacts::Guest::Alpine -variant nvx::host::artifacts::Guest::AzureLinux -variant nvx::host::artifacts::Guest::Ubuntu -method nvx::host::artifacts::Guest::default_memory_mib -method nvx::host::artifacts::Guest::distribution -method nvx::host::artifacts::Guest::distro_layer_name -method nvx::host::artifacts::Guest::distro_manifest_name -method nvx::host::artifacts::Guest::initramfs_name -method nvx::host::artifacts::Guest::name -method nvx::host::artifacts::Guest::package_manifest_name -struct nvx::host::artifacts::GuestPaths -field nvx::host::artifacts::GuestPaths::distribution -field nvx::host::artifacts::GuestPaths::distro_layer -field nvx::host::artifacts::GuestPaths::distro_manifest -field nvx::host::artifacts::GuestPaths::initramfs -field nvx::host::artifacts::GuestPaths::name -field nvx::host::artifacts::GuestPaths::package_manifest -const nvx::host::artifacts::INITRAMFS_PROVENANCE_NAME -const nvx::host::artifacts::KERNEL_NAME -const nvx::host::artifacts::KERNEL_PROVENANCE_NAME -enum nvx::host::artifacts::Layout -variant nvx::host::artifacts::Layout::Release -variant nvx::host::artifacts::Layout::Repository -const nvx::host::artifacts::MANIFEST_NAME -const nvx::host::artifacts::OPENVMM_PROVENANCE_NAME -const nvx::host::artifacts::PACKAGE_MANIFEST_SUFFIX -fn nvx::host::artifacts::absolute -fn nvx::host::artifacts::openvmm_executable mod nvx::host::cli (hidden) fn nvx::host::cli::main (hidden) mod nvx::host::config @@ -797,11 +741,6 @@ mod nvx::host::sandbox const nvx::host::sandbox::DEFAULT_ENTRYPOINT const nvx::host::sandbox::DEFAULT_HOSTNAME const nvx::host::sandbox::DEFAULT_WORKLOAD_IDENTITY -const nvx::host::sandbox::LAYER_ROLES -enum nvx::host::sandbox::LayerRole -variant nvx::host::sandbox::LayerRole::Distro -method nvx::host::sandbox::LayerRole::as_str -method nvx::host::sandbox::LayerRole::from_name enum nvx::host::sandbox::MountAccess variant nvx::host::sandbox::MountAccess::ReadOnly variant nvx::host::sandbox::MountAccess::ReadWrite @@ -829,14 +768,6 @@ variant nvx::host::sandbox::PolicyKind::Denied variant nvx::host::sandbox::PolicyKind::Writable method nvx::host::sandbox::PolicyKind::as_str method nvx::host::sandbox::PolicyKind::option -struct nvx::host::sandbox::SandboxLayer -method nvx::host::sandbox::SandboxLayer::format -method nvx::host::sandbox::SandboxLayer::gpt -method nvx::host::sandbox::SandboxLayer::new -method nvx::host::sandbox::SandboxLayer::parse -method nvx::host::sandbox::SandboxLayer::path -method nvx::host::sandbox::SandboxLayer::role -method nvx::host::sandbox::SandboxLayer::uuid struct nvx::host::sandbox::SandboxMount method nvx::host::sandbox::SandboxMount::absolute method nvx::host::sandbox::SandboxMount::absolute_policy_paths @@ -850,38 +781,6 @@ method nvx::host::sandbox::SandboxMount::parse method nvx::host::sandbox::SandboxMount::policy method nvx::host::sandbox::SandboxMount::validated method nvx::host::sandbox::SandboxMount::with_kind -mod nvx::host::sandbox::distro -const nvx::host::sandbox::distro::BINDING_VERSION -struct nvx::host::sandbox::distro::CheckedDistro -const nvx::host::sandbox::distro::DISTRO_POLICY_REVISION -struct nvx::host::sandbox::distro::DistroBinding -method nvx::host::sandbox::distro::DistroBinding::check -method nvx::host::sandbox::distro::DistroBinding::verify -enum nvx::host::sandbox::distro::DistroFormat -variant nvx::host::sandbox::distro::DistroFormat::Erofs -variant nvx::host::sandbox::distro::DistroFormat::Gpt -method nvx::host::sandbox::distro::DistroFormat::as_str -enum nvx::host::sandbox::distro::PolicyResult -variant nvx::host::sandbox::distro::PolicyResult::Accepted -variant nvx::host::sandbox::distro::PolicyResult::Rejected -struct nvx::host::sandbox::distro::PolicyVerdict -const nvx::host::sandbox::distro::PolicyVerdict::ACCEPTED -field nvx::host::sandbox::distro::PolicyVerdict::result -field nvx::host::sandbox::distro::PolicyVerdict::revision -mod nvx::host::sandbox::layer -const nvx::host::sandbox::layer::LAYER_ROLES -enum nvx::host::sandbox::layer::LayerRole -variant nvx::host::sandbox::layer::LayerRole::Distro -method nvx::host::sandbox::layer::LayerRole::as_str -method nvx::host::sandbox::layer::LayerRole::from_name -struct nvx::host::sandbox::layer::SandboxLayer -method nvx::host::sandbox::layer::SandboxLayer::format -method nvx::host::sandbox::layer::SandboxLayer::gpt -method nvx::host::sandbox::layer::SandboxLayer::new -method nvx::host::sandbox::layer::SandboxLayer::parse -method nvx::host::sandbox::layer::SandboxLayer::path -method nvx::host::sandbox::layer::SandboxLayer::role -method nvx::host::sandbox::layer::SandboxLayer::uuid mod nvx::host::sandbox::mount const nvx::host::sandbox::mount::AGGREGATE_MOUNT_TARGET const nvx::host::sandbox::mount::MAX_MOUNT_POLICY_PATHS @@ -981,37 +880,6 @@ fn nvx::host::sandbox::options::validate_run_share_targets fn nvx::host::sandbox::options::validate_sandbox_entrypoint fn nvx::host::sandbox::parse_workload_identity fn nvx::host::sandbox::require_file -mod nvx::host::sandbox::scratch -const nvx::host::sandbox::scratch::CLAIM_RELEASE_WAIT -struct nvx::host::sandbox::scratch::CheckedScratch -method nvx::host::sandbox::scratch::CheckedScratch::into_file -const nvx::host::sandbox::scratch::SCRATCH_BINDING_VERSION -struct nvx::host::sandbox::scratch::ScratchBinding -method nvx::host::sandbox::scratch::ScratchBinding::bind -method nvx::host::sandbox::scratch::ScratchBinding::check -mod nvx::host::seal -enum nvx::host::seal::FileKind -variant nvx::host::seal::FileKind::Block -variant nvx::host::seal::FileKind::Character -variant nvx::host::seal::FileKind::Directory -variant nvx::host::seal::FileKind::Fifo -variant nvx::host::seal::FileKind::Other -variant nvx::host::seal::FileKind::Regular -variant nvx::host::seal::FileKind::Socket -struct nvx::host::seal::FileSeal -method nvx::host::seal::FileSeal::device -method nvx::host::seal::FileSeal::file_id -method nvx::host::seal::FileSeal::kind -method nvx::host::seal::FileSeal::length -method nvx::host::seal::FileSeal::of -method nvx::host::seal::FileSeal::scheme -const nvx::host::seal::SEAL_VERSION -enum nvx::host::seal::SealScheme -variant nvx::host::seal::SealScheme::Linux -variant nvx::host::seal::SealScheme::Windows -struct nvx::host::seal::SealTime -field nvx::host::seal::SealTime::nanoseconds -field nvx::host::seal::SealTime::seconds mod nvx::host::snapshot struct nvx::host::snapshot::BlockGeometry field nvx::host::snapshot::BlockGeometry::length diff --git a/nvx-rs/src/host/artifacts/dir.rs b/nvx-rs/src/host/artifacts/dir.rs index 33a35cbb7..6b2563e74 100644 --- a/nvx-rs/src/host/artifacts/dir.rs +++ b/nvx-rs/src/host/artifacts/dir.rs @@ -19,7 +19,7 @@ use crate::host::runtime::{ArtifactIdentity, RuntimeIdentity}; /// The layout of a directory of NVX artifacts. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] #[serde(rename_all = "lowercase")] -pub enum Layout { +pub(crate) enum Layout { /// A repository checkout: the guest artifacts in `build/`, and OpenVMM in /// `openvmm/target/release/`, where a local build writes them and where `nvx-rs download` /// installs a release. @@ -36,7 +36,7 @@ pub enum Layout { /// `require_*` methods check them with the messages of `nvx.py`. Nothing here reads the release's /// `SOURCE-MANIFEST.json`, which `nvx.py` does not check either. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ArtifactDir { +pub(crate) struct ArtifactDir { /// The absolute directory. root: PathBuf, /// Its layout. @@ -48,14 +48,14 @@ impl ArtifactDir { /// /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) if a relative /// `root` meets an unreadable current directory. - pub fn repository(root: impl AsRef) -> Result { + pub(crate) fn repository(root: impl AsRef) -> Result { Self::new(root.as_ref(), Layout::Repository) } /// Uses an extracted release package. /// /// Fails as [`repository`](Self::repository) does. - pub fn release(root: impl AsRef) -> Result { + pub(crate) fn release(root: impl AsRef) -> Result { Self::new(root.as_ref(), Layout::Release) } @@ -67,7 +67,7 @@ impl ArtifactDir { /// other directory is a repository checkout. /// /// Fails as [`repository`](Self::repository) does. - pub fn from_dir(directory: impl AsRef) -> Result { + pub(crate) fn from_dir(directory: impl AsRef) -> Result { let directory = directory.as_ref(); let layout = if is_release_dir(directory) { Layout::Release @@ -86,18 +86,18 @@ impl ArtifactDir { } /// Returns the absolute directory. - pub fn root(&self) -> &Path { + pub(crate) fn root(&self) -> &Path { &self.root } /// Returns the directory's layout. - pub fn layout(&self) -> Layout { + pub(crate) fn layout(&self) -> Layout { self.layout } /// Returns the path of the guest artifact `name`, `artifact_path(name)` of `nvx.py`: /// `build/NAME` in a checkout, and `guest/NAME` in a release. - pub fn artifact(&self, name: &str) -> PathBuf { + pub(crate) fn artifact(&self, name: &str) -> PathBuf { let directory = match self.layout { Layout::Repository => REPO_BUILD_DIR, Layout::Release => RELEASE_GUEST_DIR, @@ -108,7 +108,7 @@ impl ArtifactDir { /// Returns the path of the OpenVMM executable, `openvmm_binary_path()` of `nvx.py`: /// `openvmm/target/release/openvmm[.exe]` in a checkout, and `bin/openvmm[.exe]` in a /// release. - pub fn openvmm(&self) -> PathBuf { + pub(crate) fn openvmm(&self) -> PathBuf { let directory = match self.layout { Layout::Repository => REPO_OPENVMM_DIR.iter().collect(), Layout::Release => PathBuf::from(RELEASE_BIN_DIR), @@ -117,27 +117,27 @@ impl ArtifactDir { } /// Returns the path of the guest kernel, which every guest boots. - pub fn kernel(&self) -> PathBuf { + pub(crate) fn kernel(&self) -> PathBuf { self.artifact(KERNEL_NAME) } /// Returns the path of a guest's initramfs. - pub fn initramfs(&self, guest: Guest) -> PathBuf { + pub(crate) fn initramfs(&self, guest: Guest) -> PathBuf { self.artifact(guest.initramfs_name()) } /// Returns the path of the package manifest of a guest's initramfs. - pub fn package_manifest(&self, guest: Guest) -> PathBuf { + pub(crate) fn package_manifest(&self, guest: Guest) -> PathBuf { self.artifact(&guest.package_manifest_name()) } /// Returns the path of a guest's distro layer, if the guest provides one. - pub fn distro_layer(&self, guest: Guest) -> Option { + pub(crate) fn distro_layer(&self, guest: Guest) -> Option { guest.distro_layer_name().map(|name| self.artifact(name)) } /// Returns the path of the manifest of a guest's distro layer, if the guest provides one. - pub fn distro_manifest(&self, guest: Guest) -> Option { + pub(crate) fn distro_manifest(&self, guest: Guest) -> Option { guest .distro_manifest_name() .map(|name| self.artifact(&name)) @@ -149,7 +149,7 @@ impl ArtifactDir { /// PATH`. /// /// [`ErrorCode::ArtifactMissing`]: crate::ErrorCode::ArtifactMissing - pub fn require_kernel(&self) -> Result { + pub(crate) fn require_kernel(&self) -> Result { required(self.kernel(), "Linux direct kernel") } @@ -160,7 +160,7 @@ impl ArtifactDir { /// found: PATH`, such as `Ubuntu Base initramfs not found: PATH`. /// /// [`ErrorCode::ArtifactMissing`]: crate::ErrorCode::ArtifactMissing - pub fn require_initramfs(&self, guest: Guest) -> Result { + pub(crate) fn require_initramfs(&self, guest: Guest) -> Result { required( self.initramfs(guest), &format!("{} initramfs", guest.distribution()), @@ -168,7 +168,7 @@ impl ArtifactDir { } /// Returns the paths of every artifact, for a caller to show or record. - pub fn paths(&self) -> ArtifactPaths { + pub(crate) fn paths(&self) -> ArtifactPaths { ArtifactPaths { layout: self.layout, root: self.root.clone(), @@ -209,7 +209,7 @@ impl ArtifactDir { /// [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) when the metadata is missing or /// unreadable, or names no valid digest of an artifact. A file of metadata that is not a /// regular file, such as a FIFO, is refused without being waited on. - pub fn runtime_identity(&self) -> Result { + pub(crate) fn runtime_identity(&self) -> Result { let [openvmm, kernel, initramfs] = match self.layout { Layout::Release => self.listed_identities()?, Layout::Repository => [ @@ -289,33 +289,33 @@ impl ArtifactDir { /// ``` #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[non_exhaustive] -pub struct ArtifactPaths { +pub(crate) struct ArtifactPaths { /// The directory's layout. - pub layout: Layout, + pub(crate) layout: Layout, /// The absolute directory. - pub root: PathBuf, + pub(crate) root: PathBuf, /// The OpenVMM executable. - pub openvmm: PathBuf, + pub(crate) openvmm: PathBuf, /// The guest kernel. - pub kernel: PathBuf, + pub(crate) kernel: PathBuf, /// The artifacts of each guest, in the order of [`Guest::ALL`]. - pub guests: Vec, + pub(crate) guests: Vec, } /// The paths of the artifacts of one guest. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[non_exhaustive] -pub struct GuestPaths { +pub(crate) struct GuestPaths { /// The guest's name, [`Guest::name`]. - pub name: &'static str, + pub(crate) name: &'static str, /// The guest's distribution, [`Guest::distribution`]. - pub distribution: &'static str, + pub(crate) distribution: &'static str, /// The initramfs. - pub initramfs: PathBuf, + pub(crate) initramfs: PathBuf, /// The package manifest of the initramfs. - pub package_manifest: PathBuf, + pub(crate) package_manifest: PathBuf, /// The distro layer, if the guest provides one. - pub distro_layer: Option, + pub(crate) distro_layer: Option, /// The manifest of the distro layer, if the guest provides one. - pub distro_manifest: Option, + pub(crate) distro_manifest: Option, } diff --git a/nvx-rs/src/host/artifacts/guest.rs b/nvx-rs/src/host/artifacts/guest.rs index 502e13888..8e0a623c1 100644 --- a/nvx-rs/src/host/artifacts/guest.rs +++ b/nvx-rs/src/host/artifacts/guest.rs @@ -6,7 +6,7 @@ use super::{DISTRO_MANIFEST_SUFFIX, PACKAGE_MANIFEST_SUFFIX}; /// A Linux guest that `nvx.py` builds and runs, as `scripts/nvx_tools/guests.py` describes it. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Guest { +pub(crate) enum Guest { /// Alpine Linux: the default guest of `nvx-rs run`, and the one that runs sandboxes. Alpine, /// Ubuntu Base, which also provides the distro layer of sandbox workloads. @@ -17,10 +17,10 @@ pub enum Guest { impl Guest { /// Every guest, in the order in which `nvx.py` lists them. - pub const ALL: [Self; 3] = [Self::Alpine, Self::Ubuntu, Self::AzureLinux]; + pub(crate) const ALL: [Self; 3] = [Self::Alpine, Self::Ubuntu, Self::AzureLinux]; /// Returns the guest's name in `nvx.py` options: `alpine`, `ubuntu`, or `azurelinux`. - pub const fn name(self) -> &'static str { + pub(crate) const fn name(self) -> &'static str { match self { Self::Alpine => "alpine", Self::Ubuntu => "ubuntu", @@ -30,7 +30,7 @@ impl Guest { /// Returns the name of the guest's distribution, as messages show it: `Alpine Linux`, /// `Ubuntu Base`, or `Azure Linux`. - pub const fn distribution(self) -> &'static str { + pub(crate) const fn distribution(self) -> &'static str { match self { Self::Alpine => "Alpine Linux", Self::Ubuntu => "Ubuntu Base", @@ -39,7 +39,7 @@ impl Guest { } /// Returns the file name of the guest's initramfs. - pub const fn initramfs_name(self) -> &'static str { + pub(crate) const fn initramfs_name(self) -> &'static str { match self { Self::Alpine => "initramfs.cpio.gz", Self::Ubuntu => "initramfs-ubuntu.cpio.gz", @@ -51,7 +51,7 @@ impl Guest { /// the `default_memory_mib` of its descriptor in `scripts/nvx_tools/guests.py`, which /// `nvx.py test-microvm` also uses, and which a test of `nvx.py` checks against the help of /// `nvx-rs run`. - pub const fn default_memory_mib(self) -> u32 { + pub(crate) const fn default_memory_mib(self) -> u32 { match self { Self::Alpine => 128, Self::Ubuntu | Self::AzureLinux => 512, @@ -59,13 +59,13 @@ impl Guest { } /// Returns the file name of the package manifest of the guest's initramfs. - pub fn package_manifest_name(self) -> String { + pub(crate) fn package_manifest_name(self) -> String { format!("{}{PACKAGE_MANIFEST_SUFFIX}", self.initramfs_name()) } /// Returns the file name of the guest's EROFS distro layer, `GUEST-distro.erofs`, if the /// guest provides one: only Ubuntu does. - pub const fn distro_layer_name(self) -> Option<&'static str> { + pub(crate) const fn distro_layer_name(self) -> Option<&'static str> { match self { Self::Ubuntu => Some("ubuntu-distro.erofs"), Self::Alpine | Self::AzureLinux => None, @@ -73,7 +73,7 @@ impl Guest { } /// Returns the file name of the manifest of the guest's distro layer, if it has one. - pub fn distro_manifest_name(self) -> Option { + pub(crate) fn distro_manifest_name(self) -> Option { self.distro_layer_name() .map(|name| format!("{name}{DISTRO_MANIFEST_SUFFIX}")) } diff --git a/nvx-rs/src/host/artifacts/mod.rs b/nvx-rs/src/host/artifacts/mod.rs index 5f4cbe30e..551dfa0f0 100644 --- a/nvx-rs/src/host/artifacts/mod.rs +++ b/nvx-rs/src/host/artifacts/mod.rs @@ -20,26 +20,35 @@ //! `scripts/nvx_tools/common.py`, so that `nvx-rs` and the `nvx.py` commands that build, //! install, benchmark, and test the guests agree on where each artifact is. +#[cfg(feature = "cli")] mod dir; +#[cfg(feature = "cli")] mod guest; +#[cfg(feature = "cli")] mod identity; mod require; +#[cfg(feature = "cli")] use std::path::{Path, PathBuf}; +#[cfg(feature = "cli")] use crate::error::{Error, Result}; -pub use self::dir::{ArtifactDir, ArtifactPaths, GuestPaths, Layout}; -pub use self::guest::Guest; +#[cfg(feature = "cli")] +pub(crate) use self::dir::{ArtifactDir, ArtifactPaths, Layout}; +#[cfg(feature = "cli")] +pub(crate) use self::guest::Guest; #[cfg(feature = "cli")] pub(crate) use self::require::require_artifact; pub(crate) use self::require::require_existing; /// Name of the release manifest at the root of a release archive or repository checkout. -pub const MANIFEST_NAME: &str = "SOURCE-MANIFEST.json"; +#[cfg(feature = "cli")] +pub(crate) const MANIFEST_NAME: &str = "SOURCE-MANIFEST.json"; /// Returns whether `directory` is in the release layout: whether it contains `bin/openvmm[.exe]`, /// `guest/vmlinux`, or `guest/initramfs.cpio.gz`. +#[cfg(feature = "cli")] fn is_release_dir(directory: &Path) -> bool { let guest = directory.join(RELEASE_GUEST_DIR); [ @@ -52,7 +61,8 @@ fn is_release_dir(directory: &Path) -> bool { } /// Returns the file name of the OpenVMM executable on this host. -pub fn openvmm_executable() -> &'static str { +#[cfg(feature = "cli")] +pub(crate) fn openvmm_executable() -> &'static str { if cfg!(windows) { "openvmm.exe" } else { @@ -62,40 +72,54 @@ pub fn openvmm_executable() -> &'static str { /// Resolves a relative path against the current directory, failing with /// [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable). -pub fn absolute(path: &Path) -> Result { +#[cfg(feature = "cli")] +pub(crate) fn absolute(path: &Path) -> Result { std::path::absolute(path).map_err(|error| { Error::unavailable(format!("cannot resolve {}", path.display())).with_source(error) }) } /// File name of the guest kernel in both layouts. -pub const KERNEL_NAME: &str = "vmlinux"; +#[cfg(feature = "cli")] +pub(crate) const KERNEL_NAME: &str = "vmlinux"; /// Suffix of the package manifest that lists the packages of an initramfs. -pub const PACKAGE_MANIFEST_SUFFIX: &str = ".packages.json"; +#[cfg(feature = "cli")] +pub(crate) const PACKAGE_MANIFEST_SUFFIX: &str = ".packages.json"; /// Suffix of the manifest of a distro layer, which binds the layer's digest and packages. -pub const DISTRO_MANIFEST_SUFFIX: &str = ".manifest.json"; +#[cfg(feature = "cli")] +pub(crate) const DISTRO_MANIFEST_SUFFIX: &str = ".manifest.json"; /// Repository-layout directory of guest artifacts. +#[cfg(feature = "cli")] pub(crate) const REPO_BUILD_DIR: &str = "build"; /// Repository-layout path of the OpenVMM build output directory. +#[cfg(feature = "cli")] const REPO_OPENVMM_DIR: [&str; 3] = ["openvmm", "target", "release"]; /// Release-layout directory of the OpenVMM executable. +#[cfg(feature = "cli")] const RELEASE_BIN_DIR: &str = "bin"; /// Release-layout directory of the guest artifacts. +#[cfg(feature = "cli")] const RELEASE_GUEST_DIR: &str = "guest"; /// Name of the list of the SHA-256 digests of the files of a release, at its root. -pub const CHECKSUMS_NAME: &str = "SHA256SUMS"; +#[cfg(feature = "cli")] +pub(crate) const CHECKSUMS_NAME: &str = "SHA256SUMS"; /// Name of the build provenance of the OpenVMM executable, beside the guest artifacts of a /// repository checkout. -pub const OPENVMM_PROVENANCE_NAME: &str = "openvmm.provenance.json"; +#[cfg(feature = "cli")] +pub(crate) const OPENVMM_PROVENANCE_NAME: &str = "openvmm.provenance.json"; /// Name of the build provenance of the guest kernel, beside it. -pub const KERNEL_PROVENANCE_NAME: &str = "vmlinux.provenance.json"; +#[cfg(feature = "cli")] +pub(crate) const KERNEL_PROVENANCE_NAME: &str = "vmlinux.provenance.json"; /// Name of the build provenance of the Alpine initramfs, beside it. -pub const INITRAMFS_PROVENANCE_NAME: &str = "initramfs.provenance.json"; +#[cfg(feature = "cli")] +pub(crate) const INITRAMFS_PROVENANCE_NAME: &str = "initramfs.provenance.json"; /// Format of the build provenance that this crate reads. +#[cfg(feature = "cli")] const PROVENANCE_FORMAT: u64 = 1; /// Longest file of install metadata that this crate reads, in bytes. A release's checksum list /// holds one line for each of its files, and a build provenance a few fields. +#[cfg(feature = "cli")] const MAX_METADATA_LEN: u64 = 1 << 20; -#[cfg(test)] +#[cfg(all(test, feature = "cli"))] mod tests; diff --git a/nvx-rs/src/host/artifacts/require.rs b/nvx-rs/src/host/artifacts/require.rs index a941a8b41..c5eaf07bf 100644 --- a/nvx-rs/src/host/artifacts/require.rs +++ b/nvx-rs/src/host/artifacts/require.rs @@ -1,12 +1,15 @@ //! The checks that the artifacts that a launch runs are regular files. -use std::path::{Path, PathBuf}; +use std::path::Path; +#[cfg(feature = "cli")] +use std::path::PathBuf; use crate::error::{Error, ErrorCode, Result}; use crate::host::paths; /// Checks that `path` is a regular file, as `require_file` of `nvx.py` does, and returns it, or /// fails with [`ErrorCode::ArtifactMissing`]. +#[cfg(feature = "cli")] pub(super) fn required(path: PathBuf, description: &str) -> Result { require_artifact(&path, description)?; Ok(path) @@ -14,6 +17,7 @@ pub(super) fn required(path: PathBuf, description: &str) -> Result { /// Checks that an artifact that a launch runs, such as the OpenVMM executable, is a regular /// file, as [`require_existing`] does, and fails with [`ErrorCode::ArtifactMissing`]. +#[cfg(feature = "cli")] pub(crate) fn require_artifact(path: &Path, description: &str) -> Result<()> { require_existing(path, description, ErrorCode::ArtifactMissing) } diff --git a/nvx-rs/src/host/cli/sandbox/options.rs b/nvx-rs/src/host/cli/sandbox/options.rs index c48a1cd5e..b8d40d705 100644 --- a/nvx-rs/src/host/cli/sandbox/options.rs +++ b/nvx-rs/src/host/cli/sandbox/options.rs @@ -14,11 +14,11 @@ use crate::host::network::{ }; use crate::host::paths; use crate::host::sandbox::distro::DistroFormat; +use crate::host::sandbox::layer::SandboxLayer; use crate::host::sandbox::mount::{MountOwner, PolicyKind}; use crate::host::sandbox::options::{PolicyPathOption, SandboxOperation}; use crate::host::sandbox::{ - DEFAULT_ENTRYPOINT, DEFAULT_HOSTNAME, DEFAULT_WORKLOAD_IDENTITY, SandboxLayer, - parse_workload_identity, + DEFAULT_ENTRYPOINT, DEFAULT_HOSTNAME, DEFAULT_WORKLOAD_IDENTITY, parse_workload_identity, }; use crate::net::{Ipv4Cidr, guest_gateway}; use crate::protocol::root::RootUpper; diff --git a/nvx-rs/src/host/cli/sandbox/tests.rs b/nvx-rs/src/host/cli/sandbox/tests.rs index ed0feeca6..0d5a01b86 100644 --- a/nvx-rs/src/host/cli/sandbox/tests.rs +++ b/nvx-rs/src/host/cli/sandbox/tests.rs @@ -93,6 +93,57 @@ fn image_options_are_only_valid_for_run_and_provision() { assert!(parse(&["--layer-format".to_owned(), "ext4".to_owned()]).is_err()); } +/// Provision records the bindings that explicit verification of the distro layer and the binding +/// of the scratch image make, which each start checks, and records no binding of a scratch image +/// for an upper in RAM. +#[cfg(any(target_os = "linux", windows))] +#[test] +fn provision_records_the_bindings_that_verification_and_the_scratch_binding_make() { + use crate::host::sandbox::layer::LayerRole; + use crate::host::sandbox::scratch::ScratchBinding; + + const UUID: &str = "11111111-1111-1111-1111-111111111111"; + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let text = |path: &Path| path.to_str().unwrap().to_owned(); + let image = root.join("distro.erofs"); + std::fs::write(&image, b"distro\n").unwrap(); + let scratch = root.join("scratch.ext4"); + std::fs::write(&scratch, b"scratch\n").unwrap(); + let layer = SandboxLayer::new(LayerRole::Distro, &text(&image), UUID); + for (name, upper) in [ + ("scratch", ["--scratch".to_owned(), text(&scratch)]), + ("ram", ["--upper".to_owned(), "ram".to_owned()]), + ] { + let state = root.join(name); + let mut arguments = vec![ + "provision".to_owned(), + "--state-dir".to_owned(), + text(&state), + "--layer".to_owned(), + format!("distro,{},{UUID}", text(&image)), + ]; + arguments.extend(upper); + let (options, unknown) = parse(&arguments).unwrap(); + assert!(unknown.is_empty(), "{unknown:?}"); + run(Some(&text(root)), options).unwrap(); + let config: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(state.join(lifecycle::CONFIG_NAME)).unwrap(), + ) + .unwrap(); + assert_eq!( + config["distro_binding"], + serde_json::to_value(DistroBinding::verify(&layer).unwrap()).unwrap(), + "{name}" + ); + let scratch_binding = match name { + "scratch" => serde_json::to_value(ScratchBinding::bind(&scratch).unwrap()).unwrap(), + _ => serde_json::Value::Null, + }; + assert_eq!(config["scratch_binding"], scratch_binding, "{name}"); + } +} + proptest! { #[test] fn policy_option_sequences_record_the_preceding_mount( diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 1a372a8cb..7c41e0d81 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -1,7 +1,8 @@ //! The host side of NVX. //! -//! - [`artifacts`] locates the OpenVMM executable and the guest kernel and initramfs, and every -//! artifact of the guests that `nvx.py` runs, in a repository checkout or a release. +//! - The private `artifacts` locates, for the CLI, the OpenVMM executable and the guest kernel and +//! initramfs, and every artifact of the guests that `nvx.py` runs, in a repository checkout or +//! a release. //! - [`runtime`] holds what every launch shares: the paths of those three files and their //! [`RuntimeIdentity`](runtime::RuntimeIdentity), the hypervisor, which //! [`probe_hypervisor`](runtime::probe_hypervisor) checks, and the @@ -28,9 +29,9 @@ //! exits and termination reaches only it, whether or not the caller started it. //! - [`sandbox`] is the launch profile of `nvx-rs sandbox`: a container over read-only EROFS //! layers and a private ext4 scratch disk, with live shares, and the checks of its options. -//! [`sandbox::distro`] binds a distro layer to what explicit verification found, so that a -//! launch checks the layer through [`seal`], which seals open files from their metadata alone, -//! and [`sandbox::scratch`] binds and claims a scratch image. +//! Its private `distro` (feature `cli`) binds a distro layer to what explicit verification +//! found, so that the CLI's launches check the layer through the private `seal`, which seals open +//! files from their metadata alone, and its private `scratch` binds and claims a scratch image. //! - [`vm`] holds a caller-owned VM directory, private to the current user, the fixed names of //! its files and endpoints, and a checked cold, capture or restore [`Prepared`](vm::Prepared) //! launch with its opened log and control credential, which the library never writes. A @@ -56,12 +57,12 @@ //! //! The host modules deny unsafe code. On Unix, the private `sys` module confines the system calls //! that the standard library lacks behind documented safe functions, which call `nix`. The -//! Windows platform module and the Windows module of [`seal`] still call `windows-sys` directly, +//! Windows platform module and the Windows module of `seal` still call `windows-sys` directly, //! and the CLI's identification of the host CPU executes `CPUID` through its `core::arch` //! intrinsic, which the crate's minimum Rust version still marks unsafe. #![deny(unsafe_code)] -pub mod artifacts; +pub(crate) mod artifacts; #[cfg(feature = "cli")] #[doc(hidden)] pub mod cli; @@ -78,7 +79,7 @@ pub mod restore; mod restore_driver; pub mod runtime; pub mod sandbox; -pub mod seal; +pub(crate) mod seal; #[cfg(feature = "cli")] pub(crate) mod session; pub mod snapshot; diff --git a/nvx-rs/src/host/sandbox/distro.rs b/nvx-rs/src/host/sandbox/distro.rs index b9df48e30..548352b7f 100644 --- a/nvx-rs/src/host/sandbox/distro.rs +++ b/nvx-rs/src/host/sandbox/distro.rs @@ -55,10 +55,10 @@ use crate::host::seal::{self, FileKind, FileSeal}; /// A change of the policy moves the revision, and [`DistroBinding::check`] refuses a binding of /// another revision than the one that its caller accepts, so a layer is verified again under the /// new policy before it launches. -pub const DISTRO_POLICY_REVISION: u32 = 1; +pub(crate) const DISTRO_POLICY_REVISION: u32 = 1; /// The version of the serde form of [`DistroBinding`] that this build writes and checks. -pub const BINDING_VERSION: u32 = 1; +pub(crate) const BINDING_VERSION: u32 = 1; /// How the operations of a binding reach files: the opens of [`seal`] and [`FileSeal::of`], or, /// in tests, opens that forbid reading the content, and seals that change a file as it is read. @@ -87,7 +87,7 @@ impl Access<'static> { /// The format of a staged distro layer. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum DistroFormat { +pub(crate) enum DistroFormat { /// An EROFS filesystem image, whose filesystem UUID the guest checks before it mounts it. Erofs, /// A GPT disk image, whose disk GUID the guest checks before it uses the disk. @@ -96,7 +96,7 @@ pub enum DistroFormat { impl DistroFormat { /// Returns the name of the format: `erofs` or `gpt`. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Erofs => "erofs", Self::Gpt => "gpt", @@ -113,7 +113,7 @@ impl fmt::Display for DistroFormat { /// The result of a distro policy for a layer. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum PolicyResult { +pub(crate) enum PolicyResult { /// The policy accepts the layer. Accepted, /// The policy refuses the layer, which no check passes. @@ -123,17 +123,17 @@ pub enum PolicyResult { /// The verdict of a distro policy for a layer: the policy's revision and its result. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PolicyVerdict { +pub(crate) struct PolicyVerdict { /// The revision of the policy that judged the layer, such as [`DISTRO_POLICY_REVISION`]. - pub revision: u32, + pub(crate) revision: u32, /// What the policy found. - pub result: PolicyResult, + pub(crate) result: PolicyResult, } impl PolicyVerdict { /// The verdict of explicit verification for a layer that it accepts: NVX's distro policy, at /// [`DISTRO_POLICY_REVISION`], accepts the layer. - pub const ACCEPTED: Self = Self { + pub(crate) const ACCEPTED: Self = Self { revision: DISTRO_POLICY_REVISION, result: PolicyResult::Accepted, }; @@ -153,7 +153,7 @@ impl PolicyVerdict { /// where `format` is `erofs` or `gpt`, and each `SEAL` is the serde form of a [`FileSeal`]. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct DistroBinding { +pub(crate) struct DistroBinding { /// [`BINDING_VERSION`]. version: u32, /// The layer's format. @@ -178,7 +178,7 @@ pub struct DistroBinding { /// that nothing modifies, renames, or replaces the layer while it is held; OpenVMM's read-only /// open of the layer shares it. Dropping it closes the file. #[derive(Debug)] -pub struct CheckedDistro { +pub(crate) struct CheckedDistro { /// The checked layer, which stays open until the caller drops it. _file: File, } @@ -202,7 +202,7 @@ impl DistroBinding { /// [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) for a layer that is not a regular /// file; and with [`ErrorKind::Io`](crate::ErrorKind::Io) if the layer cannot be read or sealed, /// or changed while it was read. - pub fn verify(layer: &SandboxLayer) -> Result { + pub(crate) fn verify(layer: &SandboxLayer) -> Result { verify_with(layer, Access::READ) } @@ -224,7 +224,11 @@ impl DistroBinding { /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a verdict that rejects the /// layer and a path without a name; and with [`ErrorKind::Io`](crate::ErrorKind::Io) if a file /// cannot be opened or sealed. - pub fn check(&self, layer: &SandboxLayer, policy_revision: u32) -> Result { + pub(crate) fn check( + &self, + layer: &SandboxLayer, + policy_revision: u32, + ) -> Result { self.check_with(layer, policy_revision, Access::GUARDED) } diff --git a/nvx-rs/src/host/sandbox/distro/tests.rs b/nvx-rs/src/host/sandbox/distro/tests.rs index 0e53a079c..80df82d14 100644 --- a/nvx-rs/src/host/sandbox/distro/tests.rs +++ b/nvx-rs/src/host/sandbox/distro/tests.rs @@ -10,7 +10,8 @@ use super::*; use crate::ErrorKind; use crate::host::config::BlockGeometry; use crate::host::paths; -use crate::host::sandbox::{LayerRole, block_geometry}; +use crate::host::sandbox::block_geometry; +use crate::host::sandbox::layer::LayerRole; use crate::host::seal::open_read; use crate::host::seal::tests::open_metadata_only; @@ -784,3 +785,29 @@ fn a_manifest_path_whose_lookup_fails_is_no_absent_manifest_for_a_binding() { .check_with(&layer, DISTRO_POLICY_REVISION, METADATA_ONLY) .unwrap(); } + +#[test] +fn a_staged_image_that_was_replaced_fails_its_check() { + // Callers stage images in a directory that only their own account can write. + let directory = tempfile::tempdir().unwrap(); + let images = directory.path().join("images"); + crate::host::vm::create_private_dir(&images).unwrap(); + let path = images.join("distro.erofs"); + fs::write(&path, b"layer").unwrap(); + let layer = SandboxLayer::new( + LayerRole::Distro, + path.to_str().unwrap(), + "01234567-89ab-cdef-0123-456789abcdef", + ); + let binding = DistroBinding::verify(&layer).unwrap(); + drop(binding.check(&layer, DISTRO_POLICY_REVISION).unwrap()); + fs::rename(&path, images.join("original.erofs")).unwrap(); + fs::write(&path, b"layer").unwrap(); + assert_eq!( + binding + .check(&layer, DISTRO_POLICY_REVISION) + .unwrap_err() + .kind(), + ErrorKind::Unavailable + ); +} diff --git a/nvx-rs/src/host/sandbox/layer.rs b/nvx-rs/src/host/sandbox/layer.rs index 20c5ad3e7..1c351ad5d 100644 --- a/nvx-rs/src/host/sandbox/layer.rs +++ b/nvx-rs/src/host/sandbox/layer.rs @@ -14,24 +14,24 @@ use crate::uuid::Uuid; /// A sandbox takes exactly one layer, the distro layer, which OpenVMM attaches as the `distro` /// sandbox block device. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum LayerRole { +pub(crate) enum LayerRole { /// The distribution, the only layer of a sandbox. Distro, } /// The layer roles that `--layer` accepts and that a launch holds: `LAYER_ROLES`. -pub const LAYER_ROLES: [LayerRole; 1] = [LayerRole::Distro]; +pub(crate) const LAYER_ROLES: [LayerRole; 1] = [LayerRole::Distro]; impl LayerRole { /// Returns the role's name: `distro`. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Distro => "distro", } } /// Returns the role that `name` names, if any, one of the [`LAYER_ROLES`]. - pub fn from_name(name: &str) -> Option { + pub(crate) fn from_name(name: &str) -> Option { LAYER_ROLES.into_iter().find(|role| role.as_str() == name) } } @@ -45,7 +45,7 @@ impl fmt::Display for LayerRole { /// A read-only layer: its role, its host image, the image's format, and the identity that the /// guest expects of it, the UUID of an EROFS filesystem or the GUID of a GPT disk. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SandboxLayer { +pub(crate) struct SandboxLayer { /// The layer's role. role: LayerRole, /// The image, with its components normalized. @@ -61,7 +61,7 @@ impl SandboxLayer { /// Returns the EROFS layer of `role` at `path` with filesystem `uuid`, which are used as /// given, except that `path` is spelled with its components alone, as /// [`Path::components`] reads them. - pub fn new(role: LayerRole, path: &str, uuid: impl Into) -> Self { + pub(crate) fn new(role: LayerRole, path: &str, uuid: impl Into) -> Self { Self { role, path: paths::normalized(Path::new(path)), @@ -73,7 +73,7 @@ impl SandboxLayer { /// Returns the GPT disk of `role` at `path` with the disk GUID `guid`, which are used as /// given, except that `path` is spelled with its components alone, as [`new`](Self::new) /// spells it. - pub fn gpt(role: LayerRole, path: &str, guid: impl Into) -> Self { + pub(crate) fn gpt(role: LayerRole, path: &str, guid: impl Into) -> Self { Self { format: DistroFormat::Gpt, ..Self::new(role, path, guid) @@ -85,7 +85,7 @@ impl SandboxLayer { /// /// The UUID must be canonical: lowercase hexadecimal digits in hyphenated groups of /// 8, 4, 4, 4, and 12. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). - pub fn parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { let fields: Vec<&str> = value.split(',').collect(); let [role, path, uuid] = fields[..] else { return Err(refused("--layer must be ROLE,PATH,EROFS_UUID")); @@ -107,23 +107,23 @@ impl SandboxLayer { } /// Returns the layer's role. - pub fn role(&self) -> LayerRole { + pub(crate) fn role(&self) -> LayerRole { self.role } /// Returns the layer's image. - pub fn path(&self) -> &Path { + pub(crate) fn path(&self) -> &Path { &self.path } /// Returns the format of the layer's image. - pub fn format(&self) -> DistroFormat { + pub(crate) fn format(&self) -> DistroFormat { self.format } /// Returns the identity that the guest expects of the layer's image: the UUID of an EROFS /// filesystem, or the GUID of a GPT disk. - pub fn uuid(&self) -> &str { + pub(crate) fn uuid(&self) -> &str { &self.uuid } } diff --git a/nvx-rs/src/host/sandbox/mod.rs b/nvx-rs/src/host/sandbox/mod.rs index 33e6b602a..2ee1cc2c4 100644 --- a/nvx-rs/src/host/sandbox/mod.rs +++ b/nvx-rs/src/host/sandbox/mod.rs @@ -4,8 +4,9 @@ //! This is the profile of `nvx-rs sandbox`: its documented limits and option semantics, with //! native Rust input validation and diagnostics: //! -//! - [`SandboxLayer`]: the distro layer of a sandbox, the one [`LayerRole`] that a sandbox takes: -//! an EROFS filesystem, as `nvx-rs sandbox` names it with `ROLE,PATH,EROFS_UUID`, or a GPT disk. +//! - The private `layer` (feature `cli`): `SandboxLayer`, the distro layer of a sandbox, the one +//! `LayerRole` that a sandbox takes: an EROFS filesystem, as `nvx-rs sandbox` names it with +//! `ROLE,PATH,EROFS_UUID`, or a GPT disk. //! - [`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, and plans the shares of a sandbox through the @@ -13,10 +14,12 @@ //! - [`options`]: the checks that `nvx-rs sandbox` and `nvx-rs run` apply to their options: the //! attribution of access-policy paths to shares, the systemd entrypoint check, the targets of //! `nvx-rs run` shares, and the managed exec environment. -//! - [`distro`]: the binding of the distro layer, which explicit verification makes from the -//! layer and its manifest, and which a launch checks from metadata alone. -//! - [`scratch`]: the binding of the scratch image, its identity, kind, and length, which a -//! launch checks from metadata alone, and the claim of the image by one sandbox at a time. +//! - The private `distro` (feature `cli`): the binding of the distro layer, which explicit +//! verification makes from the layer and its manifest, and which a launch checks from metadata +//! alone. +//! - The private `scratch` (feature `cli`): the binding of the scratch image, its identity, kind, +//! and length, which a launch checks from metadata alone, and the claim of the image by one +//! sandbox at a time. //! - The block geometry that OpenVMM presents for a block device, its length and its logical and //! physical block sizes, which the crate reads through an open file of the device's image, as //! OpenVMM reads it. @@ -32,18 +35,20 @@ //! is [`ErrorKind::Io`](crate::ErrorKind::Io), with the native I/O error and, where available, //! the path quoted with Rust's debug formatting. -pub mod distro; +#[cfg(feature = "cli")] +pub(crate) mod distro; pub(crate) mod geometry; -pub mod layer; +#[cfg(feature = "cli")] +pub(crate) mod layer; pub mod mount; pub mod options; -pub mod scratch; +#[cfg(feature = "cli")] +pub(crate) mod scratch; use std::fs::File; use std::path::Path; use self::geometry::BlockGeometry; -pub use self::layer::{LAYER_ROLES, LayerRole, SandboxLayer}; use self::mount::os_error; pub use self::mount::{MountAccess, MountKind, MountOwner, MountPolicy, PolicyKind, SandboxMount}; use crate::error::{Error, ErrorCode, Result}; diff --git a/nvx-rs/src/host/sandbox/options/tests.rs b/nvx-rs/src/host/sandbox/options/tests.rs index 829ea781a..6c3b58ae7 100644 --- a/nvx-rs/src/host/sandbox/options/tests.rs +++ b/nvx-rs/src/host/sandbox/options/tests.rs @@ -4,15 +4,18 @@ use std::fs; use std::path::PathBuf; use proptest::prelude::*; +#[cfg(feature = "cli")] use serde_json::json; +#[cfg(feature = "cli")] use sha2::{Digest, Sha256}; use super::*; use crate::ErrorKind; use crate::host::paths::{self, resolve}; -use crate::host::sandbox::SandboxLayer; +#[cfg(feature = "cli")] use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding, DistroFormat}; -use crate::host::sandbox::layer::LayerRole; +#[cfg(feature = "cli")] +use crate::host::sandbox::layer::{LayerRole, SandboxLayer}; use crate::host::sandbox::mount::{plan_mounts, validate_mounts}; fn message(result: Result) -> String { @@ -60,11 +63,13 @@ fn sandbox_rejects_systemd_entrypoint() { } /// A distro layer in a temporary directory. +#[cfg(feature = "cli")] struct Layer { /// The temporary directory. directory: tempfile::TempDir, } +#[cfg(feature = "cli")] impl Layer { fn new() -> Self { let directory = tempfile::tempdir().unwrap(); @@ -97,6 +102,7 @@ impl Layer { } } +#[cfg(feature = "cli")] fn distro_sha256() -> String { Sha256::digest(b"distro") .iter() @@ -193,6 +199,7 @@ fn environment_files_hold_a_json_array_of_strings() { ); } +#[cfg(feature = "cli")] #[test] fn distro_manifest_binds_the_layer() { let layer = Layer::new(); @@ -248,6 +255,7 @@ fn distro_manifest_binds_the_layer() { ); } +#[cfg(feature = "cli")] #[test] fn systemd_entrypoints_and_images_are_refused_independently() { let layer = Layer::new(); @@ -356,6 +364,7 @@ proptest! { } } +#[cfg(feature = "cli")] #[test] fn sandbox_allows_non_systemd_sbin_init() { let layer = Layer::new(); @@ -366,6 +375,7 @@ fn sandbox_allows_non_systemd_sbin_init() { ); } +#[cfg(feature = "cli")] #[test] fn the_entrypoint_check_needs_no_manifest_and_precedes_the_distro_verification() { let layer = Layer::new(); @@ -392,6 +402,7 @@ fn the_entrypoint_check_needs_no_manifest_and_precedes_the_distro_verification() ); } +#[cfg(feature = "cli")] #[test] fn sandbox_rejects_systemd_from_bound_distro_metadata() { let layer = Layer::new(); @@ -474,6 +485,7 @@ fn sandbox_rejects_systemd_from_bound_distro_metadata() { layer.preflight("/sbin/init").unwrap(); } +#[cfg(feature = "cli")] #[test] fn distro_manifests_are_strict_json() { let layer = Layer::new(); diff --git a/nvx-rs/src/host/sandbox/scratch.rs b/nvx-rs/src/host/sandbox/scratch.rs index e1099369a..9e1504261 100644 --- a/nvx-rs/src/host/sandbox/scratch.rs +++ b/nvx-rs/src/host/sandbox/scratch.rs @@ -61,12 +61,12 @@ use crate::host::paths; use crate::host::seal::{FileId, FileKind, FileSeal, SealScheme}; /// The version of the serde form of [`ScratchBinding`] that this build writes and checks. -pub const SCRATCH_BINDING_VERSION: u32 = 1; +pub(crate) const SCRATCH_BINDING_VERSION: u32 = 1; /// How long a check of a scratch image that should be free waits for its claim to be released: /// long enough for a process that another thread of the caller spawned to execute its program, /// and so to close its copy of a claim, as [Claims](self#claims) explains. -pub const CLAIM_RELEASE_WAIT: Duration = Duration::from_secs(1); +pub(crate) const CLAIM_RELEASE_WAIT: Duration = Duration::from_secs(1); /// How often a check tries a claim that is held again. const CLAIM_RETRY_INTERVAL: Duration = Duration::from_millis(25); @@ -105,7 +105,7 @@ impl Access<'static> { /// with the fields of a [`FileSeal`] of the same names, and no times, which the VMM changes. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ScratchBinding { +pub(crate) struct ScratchBinding { /// [`SCRATCH_BINDING_VERSION`]. version: u32, /// The host that took the binding. @@ -130,7 +130,7 @@ pub struct ScratchBinding { /// which releases the claim, and on Windows the guard, unless a VMM that inherited them still /// holds them; see [Claims](self#claims). #[derive(Debug)] -pub struct CheckedScratch { +pub(crate) struct CheckedScratch { /// The checked image, or on Windows its guard. file: File, /// The claim's handle on Windows: the image, open for writing and sharing every access, @@ -150,14 +150,14 @@ impl CheckedScratch { /// Returns the checked image, open for reading and writing, whose open file description holds /// the claim on Linux, which a VMM that inherits a copy of it keeps. #[cfg(not(windows))] - pub fn file(&self) -> &File { + pub(crate) fn file(&self) -> &File { &self.file } /// Returns the image's guard, an owned handle, which the caller then holds with the claim /// alone, since this closes the claim's handle. #[cfg(windows)] - pub fn into_file(self) -> File { + pub(crate) fn into_file(self) -> File { self.file } @@ -178,7 +178,7 @@ impl ScratchBinding { /// with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) for an image that is missing /// or not a regular file, and with [`ErrorKind::Io`](crate::ErrorKind::Io) if the image cannot /// be opened or sealed, as on hosts other than Linux and Windows. - pub fn bind(path: &Path) -> Result { + pub(crate) fn bind(path: &Path) -> Result { bind_with(&normalized(path)?, Access::WRITABLE) } @@ -199,7 +199,7 @@ impl ScratchBinding { /// [`ErrorCode::ScratchClaimed`], for an image that another launch still claims at /// `deadline`; and with [`ErrorKind::Io`](crate::ErrorKind::Io) if the image cannot be opened, /// sealed, or claimed. - pub fn check(&self, path: &Path, deadline: Instant) -> Result { + pub(crate) fn check(&self, path: &Path, deadline: Instant) -> Result { self.check_with(&normalized(path)?, deadline, Access::WRITABLE) } diff --git a/nvx-rs/src/host/sandbox/scratch/tests.rs b/nvx-rs/src/host/sandbox/scratch/tests.rs index 83f18f513..77301265b 100644 --- a/nvx-rs/src/host/sandbox/scratch/tests.rs +++ b/nvx-rs/src/host/sandbox/scratch/tests.rs @@ -585,3 +585,41 @@ fn checks_pass_after_writes_that_keep_the_length() { drop(file); drop(staged.check(&binding, Instant::now()).unwrap()); } + +#[test] +fn a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release() { + // Callers stage images in a directory that only their own account can write. + let directory = tempfile::tempdir().unwrap(); + let images = directory.path().join("images"); + crate::host::vm::create_private_dir(&images).unwrap(); + let path = images.join("scratch.ext4"); + File::create(&path).unwrap().set_len(4096).unwrap(); + let binding = ScratchBinding::bind(&path).unwrap(); + let held = binding + .check(&path, Instant::now() + CLAIM_RELEASE_WAIT) + .unwrap(); + let error = binding + .check(&path, Instant::now() + Duration::from_millis(50)) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::TimedOut); + assert_eq!(error.code(), ErrorCode::ScratchClaimed); + drop(held); + drop( + binding + .check(&path, Instant::now() + CLAIM_RELEASE_WAIT) + .unwrap(), + ); + OpenOptions::new() + .write(true) + .open(&path) + .unwrap() + .set_len(8192) + .unwrap(); + assert_eq!( + binding + .check(&path, Instant::now() + CLAIM_RELEASE_WAIT) + .unwrap_err() + .kind(), + ErrorKind::Unavailable, + ); +} diff --git a/nvx-rs/src/host/sandbox/tests/specification.rs b/nvx-rs/src/host/sandbox/tests/specification.rs index 746221464..08b6b3f6f 100644 --- a/nvx-rs/src/host/sandbox/tests/specification.rs +++ b/nvx-rs/src/host/sandbox/tests/specification.rs @@ -7,13 +7,18 @@ use std::path::PathBuf; use proptest::prelude::*; +#[cfg(feature = "cli")] use super::distro::DistroFormat; +#[cfg(feature = "cli")] +use super::layer::{LAYER_ROLES, LayerRole, SandboxLayer}; use super::mount::{parse_mount_value, validate_mount_target}; use super::*; use crate::ErrorKind; use crate::protocol::cmdline::tests::model; +#[cfg(feature = "cli")] use crate::uuid::Uuid; +#[cfg(feature = "cli")] const UUID: &str = "11111111-2222-3333-4444-555555555555"; fn refused(result: Result) { @@ -51,6 +56,7 @@ fn workload_identity_grammar() { } } +#[cfg(feature = "cli")] #[test] fn layer_values_are_role_path_uuid() { assert_eq!(LAYER_ROLES, [LayerRole::Distro]); @@ -290,6 +296,7 @@ proptest! { prop_assert_eq!(parse_workload_identity(&format!("{uid}:{gid}")).unwrap(), (uid, gid)); } + #[cfg(feature = "cli")] #[test] fn canonical_layer_uuids_round_trip(bytes in any::<[u8; 16]>()) { let uuid = Uuid::from_bytes(bytes).to_string(); diff --git a/nvx-rs/src/host/seal.rs b/nvx-rs/src/host/seal.rs index 872899994..a85b5f6d4 100644 --- a/nvx-rs/src/host/seal.rs +++ b/nvx-rs/src/host/seal.rs @@ -22,11 +22,13 @@ //! //! Other hosts cannot seal files. +#[cfg(any(feature = "cli", windows))] use std::fmt; use std::fs::File; use std::io; use std::path::Path; +#[cfg(any(feature = "cli", windows))] use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(windows)] @@ -35,12 +37,14 @@ mod windows; /// The version of the seals that this build takes. Seals of different versions never compare /// equal. -pub const SEAL_VERSION: u32 = 1; +#[cfg(any(all(feature = "cli", target_os = "linux"), windows))] +pub(crate) const SEAL_VERSION: u32 = 1; /// The platform scheme of a seal: the host that took it, which fixes what its fields mean. +#[cfg(any(feature = "cli", windows))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum SealScheme { +pub(crate) enum SealScheme { /// Linux: the device and inode, with the change time. Linux, /// Windows: the volume serial number and the file ID, without a change time. @@ -48,9 +52,10 @@ pub enum SealScheme { } /// The kind of a sealed file. +#[cfg(any(feature = "cli", windows))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum FileKind { +pub(crate) enum FileKind { /// A regular file. Regular, /// A directory. @@ -69,13 +74,14 @@ pub enum FileKind { /// A timestamp of a sealed file: whole seconds since the Unix epoch, negative before it, and the /// nanoseconds within the second. +#[cfg(any(feature = "cli", windows))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SealTime { +pub(crate) struct SealTime { /// Whole seconds since 1970-01-01 00:00:00 UTC. - pub seconds: i64, + pub(crate) seconds: i64, /// Nanoseconds within the second, below 1,000,000,000. - pub nanoseconds: u32, + pub(crate) nanoseconds: u32, } /// The seal of an open file: what the host reported about it, from its metadata alone. @@ -91,9 +97,10 @@ pub struct SealTime { /// ``` /// /// `fileId` holds 32 hexadecimal digits, and `changed` is `null` on Windows. +#[cfg(any(feature = "cli", windows))] #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct FileSeal { +pub(crate) struct FileSeal { /// [`SEAL_VERSION`]. version: u32, /// The host that took the seal. @@ -113,6 +120,7 @@ pub struct FileSeal { changed: Option, } +#[cfg(any(feature = "cli", windows))] impl FileSeal { /// Seals `file`, open, from its metadata as the host reports it now. /// @@ -121,37 +129,41 @@ impl FileSeal { /// Fails with the host's error, and with [`io::ErrorKind::Unsupported`] on hosts other than /// Linux and Windows, and on Windows for a handle that is not a file of a disk volume, such as /// a pipe. - pub fn of(file: &File) -> io::Result { + pub(crate) fn of(file: &File) -> io::Result { imp::seal(file) } /// Returns the host that took the seal. - pub fn scheme(&self) -> SealScheme { + #[cfg(feature = "cli")] + pub(crate) fn scheme(&self) -> SealScheme { self.scheme } /// Returns the device, or on Windows the volume serial number. - pub fn device(&self) -> u64 { + pub(crate) fn device(&self) -> u64 { self.device } /// Returns the inode, or on Windows the file ID. - pub fn file_id(&self) -> u128 { + pub(crate) fn file_id(&self) -> u128 { self.file_id.0 } /// Returns the kind of file. - pub fn kind(&self) -> FileKind { + #[cfg(feature = "cli")] + pub(crate) fn kind(&self) -> FileKind { self.kind } /// Returns the length in bytes. - pub fn length(&self) -> u64 { + #[cfg(feature = "cli")] + pub(crate) fn length(&self) -> u64 { self.length } /// Returns what differs first between this seal and `other`, in the order version, scheme, /// identity, kind, length, modification time, and change time, or `None` if they are equal. + #[cfg(feature = "cli")] pub(crate) fn difference(&self, other: &Self) -> Option<&'static str> { if self.version != other.version { Some("seal version") @@ -174,21 +186,25 @@ impl FileSeal { } /// A file ID, which the serde form of a seal writes as 32 lowercase hexadecimal digits. +#[cfg(any(feature = "cli", windows))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) struct FileId(pub(crate) u128); +#[cfg(any(feature = "cli", windows))] impl fmt::Display for FileId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "{:032x}", self.0) } } +#[cfg(any(feature = "cli", windows))] impl Serialize for FileId { fn serialize(&self, serializer: S) -> Result { serializer.collect_str(self) } } +#[cfg(any(feature = "cli", windows))] impl<'de> Deserialize<'de> for FileId { fn deserialize>(deserializer: D) -> Result { let digits = String::deserialize(deserializer)?; @@ -209,6 +225,7 @@ impl<'de> Deserialize<'de> for FileId { /// Windows, it also opens a directory, and denies other opens for writing or deleting the file /// until the file is closed, so that nothing modifies, renames, or replaces the file meanwhile; /// opens that only read it, such as OpenVMM's open of a read-only disk, share it. +#[cfg(feature = "cli")] pub(crate) fn open_guarded(path: &Path) -> io::Result { imp::open_guarded(path) } @@ -224,14 +241,18 @@ pub(crate) fn open_read(path: &Path) -> io::Result { mod imp { use std::fs::{File, OpenOptions}; use std::io; - use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt}; + use std::os::unix::fs::OpenOptionsExt; + #[cfg(feature = "cli")] + use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::Path; use nix::fcntl::OFlag; + #[cfg(feature = "cli")] use super::{FileId, FileKind, FileSeal, SEAL_VERSION, SealScheme, SealTime}; /// See [`FileSeal::of`]. + #[cfg(feature = "cli")] pub(super) fn seal(file: &File) -> io::Result { let metadata = file.metadata()?; let file_type = metadata.file_type(); @@ -263,6 +284,7 @@ mod imp { } /// Returns the time of `seconds` and `nanoseconds`, as the kernel reports a file's time. + #[cfg(feature = "cli")] fn time(seconds: i64, nanoseconds: i64) -> io::Result { let nanoseconds = u32::try_from(nanoseconds) .ok() @@ -280,6 +302,7 @@ mod imp { } /// See [`super::open_guarded`]. + #[cfg(feature = "cli")] pub(super) fn open_guarded(path: &Path) -> io::Result { open_read(path) } @@ -303,9 +326,11 @@ mod imp { use std::io; use std::path::Path; + #[cfg(feature = "cli")] use super::FileSeal; /// See [`FileSeal::of`]. + #[cfg(feature = "cli")] pub(super) fn seal(_file: &File) -> io::Result { Err(io::Error::new( io::ErrorKind::Unsupported, @@ -314,6 +339,7 @@ mod imp { } /// See [`super::open_guarded`]. + #[cfg(feature = "cli")] pub(super) fn open_guarded(path: &Path) -> io::Result { File::open(path) } @@ -324,5 +350,5 @@ mod imp { } } -#[cfg(all(test, any(target_os = "linux", windows)))] +#[cfg(all(test, feature = "cli", any(target_os = "linux", windows)))] pub(crate) mod tests; diff --git a/nvx-rs/src/host/seal/windows.rs b/nvx-rs/src/host/seal/windows.rs index ea0af754f..469d1d8c9 100644 --- a/nvx-rs/src/host/seal/windows.rs +++ b/nvx-rs/src/host/seal/windows.rs @@ -1,19 +1,24 @@ //! The Windows seal, from the file information of the open handle, and the opens of sealed files. use std::ffi::c_void; -use std::fs::{File, OpenOptions}; +use std::fs::File; +#[cfg(feature = "cli")] +use std::fs::OpenOptions; use std::io; use std::mem; +#[cfg(feature = "cli")] use std::os::windows::fs::OpenOptionsExt; use std::os::windows::io::AsRawHandle; use std::path::Path; use windows_sys::Win32::Foundation::{FILETIME, GetLastError, HANDLE}; use windows_sys::Win32::Storage::FileSystem::{ - BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_BACKUP_SEMANTICS, FILE_ID_INFO, - FILE_SHARE_READ, FILE_TYPE_DISK, FILE_TYPE_UNKNOWN, FileIdInfo, GetFileInformationByHandle, - GetFileInformationByHandleEx, GetFileType, + BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_ID_INFO, FILE_TYPE_DISK, + FILE_TYPE_UNKNOWN, FileIdInfo, GetFileInformationByHandle, GetFileInformationByHandleEx, + GetFileType, }; +#[cfg(feature = "cli")] +use windows_sys::Win32::Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_READ}; use super::{FileId, FileKind, FileSeal, SEAL_VERSION, SealScheme, SealTime}; @@ -94,6 +99,7 @@ fn time(time: FILETIME) -> SealTime { } /// See [`super::open_guarded`]. +#[cfg(feature = "cli")] pub(super) fn open_guarded(path: &Path) -> io::Result { OpenOptions::new() .read(true) diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index 80e3f671b..dfc83f4e3 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -27,10 +27,10 @@ //! NVX writes and reads, and in the layout in which GPT stores them. It has no dependencies, so //! the host and the guest share it. //! - [`Hypervisor`]: the hypervisors that run the VMs. -//! - [`host`] (feature `host`): the layouts of NVX's artifacts, the OpenVMM command line, the -//! lowering of network and filesystem policies, the host's NVXS connection, process and -//! platform primitives, VMs that a caller creates, starts, reattaches to, and stops in private -//! directories that it owns, and the manifests and compatibility keys of templates. +//! - [`host`] (feature `host`): the OpenVMM command line, the lowering of network and filesystem +//! policies, the host's NVXS connection, process and platform primitives, VMs that a caller +//! creates, starts, reattaches to, and stops in private directories that it owns, and the +//! manifests and compatibility keys of templates. //! - `guest` (feature `guest`, Linux): the programs that run in NVX guests, such as //! `nvx-console-pending`, the guest's NVXS connection, the libraries through which any guest //! init or agent opens the terminal on which it serves its session (`guest::terminal`), starts diff --git a/nvx-rs/tests/cli.rs b/nvx-rs/tests/cli.rs index 3fc99510c..bd3c65dc6 100644 --- a/nvx-rs/tests/cli.rs +++ b/nvx-rs/tests/cli.rs @@ -2056,12 +2056,32 @@ fn an_interrupted_start_keeps_its_claim_until_recovery_confirms_exit() { .join("build") .join("fake-openvmm-state-blocks.json"); assert!(!blocks.exists()); - let scratch_binding: nvx::host::sandbox::scratch::ScratchBinding = - serde_json::from_value(root.json("state/config.json")["scratch_binding"].clone()).unwrap(); - let error = scratch_binding - .check(&scratch, Instant::now() + Duration::from_millis(100)) - .unwrap_err(); - assert_eq!(error.code(), nvx::ErrorCode::ScratchClaimed); + // A run of another sandbox on the same images finds the scratch image claimed. + let sandbox_run: Vec = vec![ + "sandbox".into(), + "run".into(), + "--layer".into(), + format!( + "distro,{},11111111-1111-1111-1111-111111111111", + distro.to_str().unwrap() + ), + "--scratch".into(), + scratch.to_str().unwrap().to_owned(), + "--entrypoint".into(), + "/bin/echo".into(), + ]; + let output = run(&root, &sandbox_run); + assert_eq!(output.status.code(), Some(1), "{}", text(&output.stderr)); + assert_eq!( + text(&output.stderr), + error_line( + "scratch-claimed", + &format!( + "another sandbox claims the ext4 scratch image {}", + scratch.to_str().unwrap() + ), + ) + ); assert_eq!(fs::read(&distro).unwrap(), b"distro\n"); assert_eq!(fs::read(&scratch).unwrap(), b"scratch\n"); @@ -2088,11 +2108,9 @@ fn an_interrupted_start_keeps_its_claim_until_recovery_confirms_exit() { assert!(process.has_exited().unwrap()); assert!(!journal.exists()); assert!(!root.below("state/control.capability").exists()); - assert!( - scratch_binding - .check(&scratch, Instant::now() + Duration::from_secs(2)) - .is_ok() - ); + // The claim went with the VM: a run on the same images claims the scratch image again. + let output = run(&root, &sandbox_run); + assert!(output.status.success(), "{}", text(&output.stderr)); assert_eq!(transition(&root, "deprovision", "state", &[]), succeeded()); assert_eq!(fs::read(&distro).unwrap(), b"distro\n"); assert_eq!(fs::read(&scratch).unwrap(), b"scratch\n"); @@ -2680,13 +2698,17 @@ fn run_guides_failed_cold_boots_on_cpus_that_no_built_in_profile_serves() { #[test] fn run_boots_each_guest_with_its_default_memory() { let root = Root::new(); - for guest in nvx::host::artifacts::Guest::ALL { - let initramfs = root.path.join("build").join(guest.initramfs_name()); - fs::write(initramfs, "initramfs\n").unwrap(); - let arguments = ["run", "--guest", guest.name(), "--dry-run"].map(str::to_owned); + // Each guest of `nvx.py`, its initramfs, and the RAM in MiB with which `run` boots it. + for (guest, initramfs, memory_mib) in [ + ("alpine", "initramfs.cpio.gz", 128), + ("ubuntu", "initramfs-ubuntu.cpio.gz", 512), + ("azurelinux", "initramfs-azurelinux.cpio.gz", 512), + ] { + fs::write(root.path.join("build").join(initramfs), "initramfs\n").unwrap(); + let arguments = ["run", "--guest", guest, "--dry-run"].map(str::to_owned); let output = run(&root, &arguments); assert!(output.status.success(), "{}", text(&output.stderr)); - let memory = format!(" --memory {}M ", guest.default_memory_mib()); + let memory = format!(" --memory {memory_mib}M "); assert!(text(&output.stdout).contains(&memory), "{guest}"); } } diff --git a/nvx-rs/tests/cli/artifacts.rs b/nvx-rs/tests/cli/artifacts.rs index 965af130b..4dda8954a 100644 --- a/nvx-rs/tests/cli/artifacts.rs +++ b/nvx-rs/tests/cli/artifacts.rs @@ -3,7 +3,6 @@ //! guest kernel, and every guest in the order of `nvx.py`, with its initramfs, package manifest, //! and distro layer and manifest, or `null`. -use nvx::host::artifacts::Guest; use serde_json::{Value, json}; use crate::harness::{LINE_END, Root}; @@ -86,7 +85,7 @@ fn artifacts_list_every_guest_in_order() { .iter() .map(|guest| guest["name"].as_str().unwrap()) .collect(); - assert_eq!(names, Guest::ALL.map(Guest::name), "{name}"); + assert_eq!(names, ["alpine", "ubuntu", "azurelinux"], "{name}"); // Two spaces indent each level, and the members are sorted, as Python writes them. let layout = ensure_ascii(&serde_json::to_string_pretty(&expected).unwrap()) + "\n"; assert_eq!(outcome.stdout(), layout.replace('\n', LINE_END), "{name}"); diff --git a/nvx-rs/tests/cli/lifecycle.rs b/nvx-rs/tests/cli/lifecycle.rs index 6ffecd2fb..07bfee820 100644 --- a/nvx-rs/tests/cli/lifecycle.rs +++ b/nvx-rs/tests/cli/lifecycle.rs @@ -12,9 +12,6 @@ use nvx::contract::{ BOOT_CONTRACT_VERSION, CONTROL_CONTRACT_REVISION, CONTROL_SESSION_PROTOCOL_VERSION, }; use nvx::host::process::{Opened, Process, ProcessRef}; -use nvx::host::sandbox::distro::DistroBinding; -use nvx::host::sandbox::scratch::ScratchBinding; -use nvx::host::sandbox::{LayerRole, SandboxLayer}; use serde_json::{Value, json}; #[cfg(unix)] @@ -114,28 +111,56 @@ pub(crate) fn child(root: &str, writable: bool) -> Value { }) } +/// The platform scheme of the seals that bindings hold on this host. +const SEAL_SCHEME: &str = if cfg!(windows) { "windows" } else { "linux" }; + +/// Asserts that `seal`, the seal of a file in a binding that provision recorded, seals the file at +/// `path` as it is: a regular file of its length, sealed on this host. +fn assert_seal(seal: &Value, path: &Path) { + assert_eq!(seal["version"], 1, "{seal}"); + assert_eq!(seal["scheme"], SEAL_SCHEME, "{seal}"); + assert_eq!(seal["kind"], "regular", "{seal}"); + assert_eq!(seal["length"], fs::metadata(path).unwrap().len(), "{seal}"); +} + +/// Asserts that `binding`, the distro binding that provision recorded, binds the distro layer of +/// the root as it is, as explicit verification of the layer, which has no manifest, binds it. +fn assert_distro_binding(root: &Root, binding: &Value) { + assert_eq!(binding["version"], 1, "{binding}"); + assert_eq!(binding["format"], "erofs", "{binding}"); + assert_eq!(binding["identity"], UUID, "{binding}"); + assert_seal(&binding["image"], &root.below("distro.erofs")); + assert_eq!(binding["manifest"], Value::Null, "{binding}"); + assert_eq!( + binding["policy"], + json!({"revision": 1, "result": "accepted"}), + "{binding}" + ); +} + /// Asserts that `config`, a configuration that provision recorded, holds the bindings of the -/// distro layer and the scratch image of the root as they are, as explicit verification of the -/// layer and the binding of the image make them, and returns it without them. +/// distro layer and the scratch image of the root as they are, and returns it without them. +/// +/// The test reads what it can tell of the images from outside: their formats, identities, kinds +/// and lengths, the distro policy's verdict and the manifest's absence. Each start checks the +/// rest of the seals, the files' identities and times, against the images, and the unit tests of +/// `nvx-rs sandbox` check that provision records the very bindings that explicit verification of +/// the layer and the binding of the image make. pub(crate) fn assert_bindings(root: &Root, mut config: Value) -> Value { let members = config.as_object_mut().unwrap(); let distro = members.remove("distro_binding").expect("a distro binding"); let scratch = members .remove("scratch_binding") .expect("a scratch binding"); - let layer = SandboxLayer::new( - LayerRole::Distro, - root.below("distro.erofs").to_str().unwrap(), - UUID, - ); - assert_eq!( - distro, - serde_json::to_value(DistroBinding::verify(&layer).unwrap()).unwrap() - ); + assert_distro_binding(root, &distro); + let mut fields: Vec<&String> = scratch.as_object().unwrap().keys().collect(); + fields.sort(); assert_eq!( - scratch, - serde_json::to_value(ScratchBinding::bind(&root.below("scratch.ext4")).unwrap()).unwrap() + fields, + ["device", "fileId", "kind", "length", "scheme", "version"], + "{scratch}" ); + assert_seal(&scratch, &root.below("scratch.ext4")); config } @@ -645,15 +670,7 @@ fn lifecycle_of_a_sandbox_whose_upper_is_in_ram() { .unwrap() .remove("distro_binding") .expect("a distro binding"); - let layer = SandboxLayer::new( - LayerRole::Distro, - root.below("distro.erofs").to_str().unwrap(), - UUID, - ); - assert_eq!( - distro, - serde_json::to_value(DistroBinding::verify(&layer).unwrap()).unwrap() - ); + assert_distro_binding(&root, &distro); let mut expected = minimal_config(); for (name, value) in [ ("upper", json!({"kind": "ram", "max_memory_percent": 50})), diff --git a/nvx-rs/tests/security_checklist.rs b/nvx-rs/tests/security_checklist.rs index f486d918a..90b7e1f19 100644 --- a/nvx-rs/tests/security_checklist.rs +++ b/nvx-rs/tests/security_checklist.rs @@ -1,15 +1,16 @@ //! The checklist of the security requirements that the host's documentation states, through the -//! public API alone: the guest paths of an export, the checks of a staged image and of a scratch -//! image's claim, the stability of exported objects, the privacy of a VM's directory and outputs, -//! and the proofs of a VM's liveness, of its endpoint's owner and of its recovery from saved -//! values, and the strictness of a snapshot's directory. +//! public API alone: the guest paths of an export, the stability of exported objects, the privacy +//! of a VM's directory and outputs, and the proofs of a VM's liveness, of its endpoint's owner and +//! of its recovery from saved values, and the strictness of a snapshot's directory. The checks of +//! a staged image and of a scratch image's claim, which only NVX's CLI makes, are unit tests of +//! `host::sandbox::distro` and `host::sandbox::scratch`. //! //! Each check is short and runs no VM: a process of the test's own, an idle copy of the test //! binary, stands in for a VM's process where one is needed. #![cfg(all(feature = "host", any(target_os = "linux", windows)))] use std::error::Error as _; -use std::fs::{self, File, OpenOptions}; +use std::fs::{self, File}; use std::io::{self, Read}; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; @@ -22,12 +23,8 @@ use nvx::host::filesystem::Owner; use nvx::host::filesystem::{self, ExportRequest, Mapping, ReservedTargets, Target}; use nvx::host::process::{BootId, ProcessRef}; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; -use nvx::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding}; -use nvx::host::sandbox::layer::{LayerRole, SandboxLayer}; -use nvx::host::sandbox::scratch::{CLAIM_RELEASE_WAIT, ScratchBinding}; -use nvx::host::seal::FileSeal; use nvx::host::{platform, snapshot, transport, vm}; -use nvx::{ErrorCode, ErrorKind, Hypervisor}; +use nvx::{ErrorKind, Hypervisor}; /// Plans the export of `mappings` into the blockless root. fn plan(mappings: &[Mapping<'_>]) -> nvx::Result> { @@ -144,65 +141,6 @@ fn private_directory() -> (tempfile::TempDir, vm::VmDir) { (directory, vm_dir) } -#[test] -fn a_staged_image_that_was_replaced_fails_its_check() { - // Callers stage images in a directory that only their own account can write. - let (_directory, images) = private_directory(); - let path = images.path().join("distro.erofs"); - fs::write(&path, b"layer").unwrap(); - let layer = SandboxLayer::new( - LayerRole::Distro, - path.to_str().unwrap(), - "01234567-89ab-cdef-0123-456789abcdef", - ); - let binding = DistroBinding::verify(&layer).unwrap(); - drop(binding.check(&layer, DISTRO_POLICY_REVISION).unwrap()); - fs::rename(&path, images.path().join("original.erofs")).unwrap(); - fs::write(&path, b"layer").unwrap(); - assert_eq!( - binding - .check(&layer, DISTRO_POLICY_REVISION) - .unwrap_err() - .kind(), - ErrorKind::Unavailable - ); -} - -#[test] -fn a_scratch_image_is_claimed_by_one_launch_and_checked_again_after_a_release() { - let (_directory, images) = private_directory(); - let path = images.path().join("scratch.ext4"); - File::create(&path).unwrap().set_len(4096).unwrap(); - let binding = ScratchBinding::bind(&path).unwrap(); - let held = binding - .check(&path, Instant::now() + CLAIM_RELEASE_WAIT) - .unwrap(); - let error = binding - .check(&path, Instant::now() + Duration::from_millis(50)) - .unwrap_err(); - assert_eq!(error.kind(), ErrorKind::TimedOut); - assert_eq!(error.code(), ErrorCode::ScratchClaimed); - drop(held); - drop( - binding - .check(&path, Instant::now() + CLAIM_RELEASE_WAIT) - .unwrap(), - ); - OpenOptions::new() - .write(true) - .open(&path) - .unwrap() - .set_len(8192) - .unwrap(); - assert_eq!( - binding - .check(&path, Instant::now() + CLAIM_RELEASE_WAIT) - .unwrap_err() - .kind(), - ErrorKind::Unavailable, - ); -} - #[test] fn a_vm_directory_is_private_and_never_restricted_in_place() { let (_directory, dir) = private_directory(); @@ -256,6 +194,55 @@ fn a_vm_directory_is_private_and_never_restricted_in_place() { assert_eq!(acl(), shared); } +/// Returns the identity of the file that `file` holds open: its device and inode on Linux. +#[cfg(target_os = "linux")] +fn file_id(file: &File) -> (u64, u128) { + use std::os::unix::fs::MetadataExt; + + let metadata = file.metadata().unwrap(); + (metadata.dev(), u128::from(metadata.ino())) +} + +/// Returns the identity of the file that `file` holds open: on Windows, the volume serial number +/// and the 128-bit file ID of its `FILE_ID_INFO`, or, on a volume that has no file ID information +/// or that reports a zero ID, those of its `BY_HANDLE_FILE_INFORMATION`. +#[cfg(windows)] +fn file_id(file: &File) -> (u64, u128) { + use std::os::windows::io::AsRawHandle; + + use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::Storage::FileSystem::{ + BY_HANDLE_FILE_INFORMATION, FILE_ID_INFO, FileIdInfo, GetFileInformationByHandle, + GetFileInformationByHandleEx, + }; + + let handle = file.as_raw_handle() as HANDLE; + let mut id = FILE_ID_INFO::default(); + // SAFETY: the handle is open for as long as `file` is borrowed, and the buffer is a + // FILE_ID_INFO of the size that is passed. + let has_id = unsafe { + GetFileInformationByHandleEx( + handle, + FileIdInfo, + (&raw mut id).cast(), + std::mem::size_of::() as u32, + ) + } != 0; + let file_id = u128::from_le_bytes(id.FileId.Identifier); + if has_id && file_id != 0 { + return (id.VolumeSerialNumber, file_id); + } + let mut information = BY_HANDLE_FILE_INFORMATION::default(); + // SAFETY: the handle is open for as long as `file` is borrowed, and the output pointer is + // valid. + let read = unsafe { GetFileInformationByHandle(handle, &mut information) }; + assert_ne!(read, 0, "{}", io::Error::last_os_error()); + ( + u64::from(information.dwVolumeSerialNumber), + (u128::from(information.nFileIndexHigh) << 32) | u128::from(information.nFileIndexLow), + ) +} + /// A cold configuration that creation checks without reading the host. fn cold() -> VmConfig { VmConfig { @@ -294,12 +281,7 @@ fn a_launch_takes_a_new_private_log_and_keeps_every_entry_that_it_did_not_create let prepared = vm::create(&runtime(), &cold(), None, &dir).unwrap(); let log = prepared.outputs().log.clone(); - let opened = FileSeal::of(prepared.log()).unwrap(); - let named = FileSeal::of(&File::open(&log).unwrap()).unwrap(); - assert_eq!( - (opened.device(), opened.file_id()), - (named.device(), named.file_id()) - ); + assert_eq!(file_id(prepared.log()), file_id(&File::open(&log).unwrap())); #[cfg(target_os = "linux")] { use std::os::unix::fs::PermissionsExt;