From fb903d559382b4ff529c34be6b6a64d04608b946 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Thu, 12 Nov 2026 07:20:41 -0800 Subject: [PATCH] guest: delete the space items that only tests use Once the old layered init, the exec engine and the process modules left, the guest's space kept many public items that no NVX program calls: nvx-init and the managed agent map through the mapper's apply_with, never tear a mapper down, and read none of the root's reports. SL6.13b (#1127) makes the space crate-private, which would leave these items dead in every feature set, so this commit deletes them first, with what the compiler then finds unused, as SL6.11d in #1123 records. Deleted: - guest::view's DEFAULT_HOSTNAME and MAX_HOSTNAME_BYTES, which nothing used. - The mapper's apply, check, is_latched, teardown and teardown_with, and bind and bind_with, with guest::mappings' re-export of bind. apply_with, unmount_with, bind_retained_with and unbind_with stay, and take the documentation of what they implemented. - Root's is_latched, first_partition, lowers, upper, overlay and map, with the private apply and mapping_mount that only map called, the accessors of Lower, AssemblyError, TeardownError and Unmount, and Unmount's path, which only its accessor read. Root keeps its fields, which its assembly, teardown and Debug form read, and mount_runtime's guard against a mapping beneath /run loses the variant that only map recorded. A root mounts its live shares through its mapper, and the caller maps its host mappings through a mapper of its own, as nvx-init does. - Applied's hostname, machine_id, resolver and into_parts, with the fields that only they read, and the seam result's matching fields; AppliedTeardownError's binds and root, with the legacy binds field, always None, which only the binds accessor exposed; and ApplyError::MachineId, which nothing constructed, with its arm in nvx-init's status classes. Tests: - Tests read private fields directly, or map and unmount through apply_with, unmount_with and bind_retained_with, as NVX's programs do. The tests of a clone's application, which can't read AssemblyError's field, use root::tests::assembly_failure. - mappings_apply_beneath_the_overlay_in_order_and_go_first_at_the_teardown, a_call_after_a_mapping_is_refused_and_leaves_it_reachable and an_applied_root_keeps_its_view_binds_when_transferred_into_parts retire with Root::map and into_parts. - The root's tests of a mapping that a rename moved and of shares beneath /run take an aggregate's child, a mapping of the root's mapper, in place of map's: a_call_after_a_share_or_a_child_... and shares_and_children_beneath_run_... take the place of the tests that named a mapping. the_host_export_..._unmounted_after_the_live_shares loses its mappings, and the privileged tests map through a mapper of the root, as nvx-init makes one. - the_record_s_view_is_applied_once_with_its_own_dns loses the machine ID and resolver that the application no longer reports. - The doc examples of guest::root and guest::mappings::mapper become the unit tests a_layered_root_s_steps_compile and a_mapper_s_steps_compile, without the deleted calls; the case matrix names them, the renamed test, and the activation example's new line. The public API snapshot loses 47 hidden lines, and adds none. The README and the module docs stop naming what left, and no longer say that a layered root mounts or keeps host mappings: the caller's mapper does. MappingTable's docs and the mapper's no longer name a mapper's teardown, which left: the table unmounts nothing, and a layered root's teardown unmounts its mapper's mappings through unmount_with. Part of #1123 (SL6.11d). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/acceptance-case-matrix.md | 10 +- nvx-rs/README.md | 44 +- nvx-rs/public-api/linux.txt | 47 -- nvx-rs/src/guest/activation.rs | 7 +- nvx-rs/src/guest/activation/state/apply.rs | 105 +--- .../src/guest/activation/state/apply/tests.rs | 47 +- nvx-rs/src/guest/init/boot.rs | 4 +- nvx-rs/src/guest/init/boot/tests.rs | 11 +- nvx-rs/src/guest/mappings.rs | 8 +- nvx-rs/src/guest/mappings/mapper.rs | 163 ++---- nvx-rs/src/guest/mappings/mapper/tests.rs | 76 ++- .../guest/mappings/mapper/tests/privileged.rs | 73 ++- nvx-rs/src/guest/mappings/table.rs | 8 +- nvx-rs/src/guest/mod.rs | 2 +- nvx-rs/src/guest/root.rs | 234 ++------ nvx-rs/src/guest/root/share.rs | 49 +- nvx-rs/src/guest/root/tests.rs | 534 +++++++----------- nvx-rs/src/guest/root/tests/privileged.rs | 81 +-- nvx-rs/src/guest/root/tests/runtime.rs | 57 +- nvx-rs/src/guest/root/tests/switch.rs | 2 +- nvx-rs/src/guest/root/tests/view.rs | 60 +- nvx-rs/src/guest/view.rs | 4 - 22 files changed, 533 insertions(+), 1093 deletions(-) diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index ae2834d2a..88890c696 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -222,7 +222,7 @@ The adapter's README said what each proved. | --- | --- | --- | | `a_clone_binds_the_resolver_of_its_policy_s_dns_server_read_only` | rewritten | `test-microvm --scenario untiered-network-policy`, whose restored guests' resolvers name exactly the DNS server of their policies; `test-microvm --scenario network-snapshot`; `nvx-rs/src/guest/root/tests/view.rs::actual_resolver_failures_keep_the_network_class_and_root_owned_bind` | | `a_clone_maps_an_aggregate_of_directories_and_files_beneath_its_root_before_its_sandbox_starts` | rewritten | `test-microvm --scenario platform-aggregate-export`; `nvx-rs/src/guest/mappings/table/tests.rs::mappings_keep_the_paths_and_the_access_of_their_entries` | -| `a_clone_of_a_template_with_a_nic_maps_its_export_beside_its_resolver` | rewritten | `nvx-rs/src/guest/activation/state/apply/tests.rs::the_record_s_view_is_applied_once_with_its_own_dns_and_returned_machine_id`; `test-microvm --scenario filesystem-snapshot` | +| `a_clone_of_a_template_with_a_nic_maps_its_export_beside_its_resolver` | rewritten | `nvx-rs/src/guest/activation/state/apply/tests.rs::the_record_s_view_is_applied_once_with_its_own_dns`; `test-microvm --scenario filesystem-snapshot` | | `a_clone_reads_no_more_of_its_disks_than_its_root_needs` | rewritten | A restore reads no content of its disks or snapshot: `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_never_reopens_snapshot_paths_or_reads_runtime_artifacts`; `nvx-rs/src/host/sandbox/distro/tests.rs::the_staged_check_reads_no_content`; `nvx-rs/src/host/sandbox/scratch/tests.rs::checks_claim_the_image_without_reading_it`. The adapter's in-guest read statistics retire | | `a_clone_refuses_a_scratch_image_whose_upper_an_earlier_clone_used` | rewritten | `nvx-rs/src/guest/root/tests.rs::a_fresh_upper_s_directories_must_be_missing_or_empty` | | `a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready` | retired | Executions that name their identities leave with the exec engine (#1087) | @@ -230,7 +230,7 @@ The adapter's README said what each proved. | `a_clone_s_time_settles_after_it_is_ready` | rewritten | `test-microvm --scenario time-host-correction`; `test-microvm --scenario restore-downtime` | | `a_clone_whose_activation_fails_ends_before_it_is_ready` | rewritten | `nvx-rs/src/host/restore/tests.rs::restore_failures_attribute_the_existing_policy_free_evidence_classes`; `nvx-rs/src/host/restore/tests.rs::restore_exit_reports_keep_guest_status_classification_and_actual_details`; `nvx-rs/src/guest/init/boot/tests.rs::a_failed_activation_ends_the_vm_with_the_status_of_its_class`; `nvx-rs/src/guest/managed_agent/session/tests.rs::a_failed_restored_activation_gives_init_the_status_of_its_class`; `nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs::a_restore_that_fails_before_its_guest_is_ready_reports_its_attribution`; `test-microvm --scenario snapshot-core`, whose restore over a zeroed scratch image ends its VM before its workload runs, with nvx-init's status of an unusable root, 54, which the CLI attributes to the request. Quarantine and leases retire (#1121) | | `a_clone_whose_mapping_fails_ends_before_its_sandbox_starts` | rewritten | `nvx-rs/src/guest/mappings/mapper/tests.rs::a_failure_at_any_step_leaves_nothing_mounted`; `nvx-rs/src/guest/mappings/table/tests.rs::a_failed_mapping_keeps_those_before_it_and_latches_the_table` | -| `a_clone_whose_view_cannot_be_set_ends_before_it_is_ready` | rewritten | `nvx-rs/src/guest/activation/state/apply/tests.rs::the_record_s_view_is_applied_once_with_its_own_dns_and_returned_machine_id`; `nvx-rs/src/guest/root/tests/view.rs::a_later_bind_failure_stays_owned_and_its_teardown_removes_the_earlier_bind` | +| `a_clone_whose_view_cannot_be_set_ends_before_it_is_ready` | rewritten | `nvx-rs/src/guest/activation/state/apply/tests.rs::the_record_s_view_is_applied_once_with_its_own_dns`; `nvx-rs/src/guest/root/tests/view.rs::a_later_bind_failure_stays_owned_and_its_teardown_removes_the_earlier_bind` | | `a_clone_whose_vm_exits_once_a_stop_found_it_fails_before_its_lease_is_released` | retired | Clone records and leases leave with the old clone path (#1121). A stop that finds an exit: `nvx-rs/src/host/vm/tests.rs::stop_preserves_report_results_after_a_real_self_exit` | | `a_forward_whose_port_another_socket_holds_fails_the_clone_as_the_host_s` | rewritten | `test-microvm --scenario platform-network-policy`; `nvx-rs/src/host/restore/tests.rs::restore_failures_attribute_the_existing_policy_free_evidence_classes` | | `a_generation_is_eligible_by_the_clock_of_its_capture` | rewritten | `nvx-rs/src/host/snapshot/eligibility/tests.rs::within_one_boot_the_downtime_is_the_host_monotonic_clocks`; `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_reports_early_and_expired_time_as_unavailable` | @@ -478,21 +478,21 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/activation/mod.rs - activation (line 130)` | kept | | | `nvx-rs/src/exec/mod.rs - exec (line 113)` | retired | The exec engine leaves (SL6.11a) | | `nvx-rs/src/gpt/mod.rs - gpt (line 43)` | kept | | -| `nvx-rs/src/guest/activation.rs - guest::activation (line 158)` | kept | SL6.11c rewrote it on `guest::snapshot::capture`, when its capture preparation and admission fence left (#1087) | +| `nvx-rs/src/guest/activation.rs - guest::activation (line 157)` | kept | SL6.11c rewrote it on `guest::snapshot::capture`, when its capture preparation and admission fence left (#1087) | | `nvx-rs/src/guest/activation/state.rs - guest::activation::state::Activation (line 56)` | kept | | | `nvx-rs/src/guest/cgroup.rs - guest::cgroup (line 36)` | retired | `guest::cgroup` leaves (SL6.11b) | | `nvx-rs/src/guest/entry.rs - guest::entry (line 90)` | retired | `guest::entry` leaves (SL6.11b) | | `nvx-rs/src/guest/exec.rs - guest::exec (line 79)` | retired | `guest::exec` leaves (SL6.11b) | | `nvx-rs/src/guest/exec.rs - guest::exec::Execution::adopt (line 619)` | retired | `guest::exec` leaves (SL6.11b) | | `nvx-rs/src/guest/gpt.rs - guest::gpt (line 18)` | kept | The guest's space becomes private (#1127), whose PR moves it to the space's unit tests | -| `nvx-rs/src/guest/mappings/mapper.rs - guest::mappings::mapper (line 107)` | kept | The guest's space becomes private (#1127), whose PR moves it to the space's unit tests | +| `nvx-rs/src/guest/mappings/mapper.rs - guest::mappings::mapper (line 107)` | kept | SL6.11d (#1123) moved it to the mapper's unit tests, `nvx-rs/src/guest/mappings/mapper/tests.rs::a_mapper_s_steps_compile`, which map and unmount through the seams that NVX's programs use, since the mapper's `apply` and `teardown`, which only tests used, left | | `nvx-rs/src/guest/mappings/table.rs - guest::mappings::table::MappingTable (line 47)` | kept | | | `nvx-rs/src/guest/mod.rs - guest (line 34)` | kept | The guest-agent guidelines quote it | | `nvx-rs/src/guest/nvxs.rs - guest::nvxs (line 22)` | kept | | | `nvx-rs/src/guest/process.rs - guest::process (line 12)` | retired | `guest::process` leaves (SL6.11b) | | `nvx-rs/src/guest/process.rs - guest::process (line 39)` | retired | `guest::process` leaves (SL6.11b) | | `nvx-rs/src/guest/reseed.rs - guest::reseed (line 17)` | kept | | -| `nvx-rs/src/guest/root.rs - guest::root (line 126)` | kept | SL6.11b removed its bootstrap, which left with the cgroups. The guest's space becomes private (#1127), whose PR moves it to the space's unit tests | +| `nvx-rs/src/guest/root.rs - guest::root (line 126)` | kept | SL6.11b removed its bootstrap, which left with the cgroups. SL6.11d (#1123) moved it to the root's unit tests, `nvx-rs/src/guest/root/tests.rs::a_layered_root_s_steps_compile`, without the first partition's bytes, the layers and `Root::map`, which only tests used, and which left | | `nvx-rs/src/guest/snapshot.rs - guest::snapshot (line 25)` | kept | The guest-agent guidelines quote it | | `nvx-rs/src/guest/spawn.rs - guest::spawn (line 41)` | retired | `guest::spawn` leaves (SL6.11b) | | `nvx-rs/src/guest/spawn.rs - guest::spawn::LaunchStage::from_code (line 201)` | retired | `LaunchStage::from_code` and its test, `launch_stages_keep_their_numbers`, leave with `guest::spawn` (SL6.11b). The stages of nvx-init's executions keep their numbers in the crate's own fork boundary, whose launch report `nvx-rs/src/guest/init/tests/exec.rs::native_workload_cwd_accepts_procfs_links_but_closes_inherited_descriptors` reads from a real launch | diff --git a/nvx-rs/README.md b/nvx-rs/README.md index e63e423a1..d29647494 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -130,7 +130,7 @@ changes only when the module's purpose does. | [`nvx::guest::snapshot`](#nvxguestsnapshot) | x86_64: bounded Fresh capture with inline restore repair and staged failure dispositions, without execution control | | [`nvx::guest::activation`](#nvxguestactivation) | x86_64: fence-free activation of each clone that its capture repaired | | [`nvx::guest::reseed`](#nvxguestreseed) | A restore's fresh entropy, and the reseed of the kernel's random number generator with it | -| [`nvx::guest::root`](#nvxguestroot) | The operations of a layered root: its assembly, shares, export, mappings, and teardown | +| [`nvx::guest::root`](#nvxguestroot) | The operations of a layered root: its assembly, shares, export, and teardown | | [`nvx::guest::view`](#nvxguestview) | The VM-wide hostname, machine ID, and single-server resolver, whose binds the layered root owns | | [`nvx::guest::gpt`](#nvxguestgpt) | `read_disk`: the partition table of a GPT disk, read and checked through its block device | @@ -1120,11 +1120,12 @@ restricted `nosuid`, `nodev`, and read-only when asked, `mount_setattr`, before do no work; the first that fails is reported with its index as an `ApplyError`, and latches the mapper. -`Mapper::teardown` detaches a mapping that a rename moved and that the attach -failed to detach, then unmounts the mappings in reverse order, and reports each -failure. +A layered root's teardown, which unmounts its mapper's mappings, first detaches +a mapping that a rename moved and that the attach failed to detach, then +unmounts the mappings in reverse order, and reports each failure. -`bind` applies one runtime bind through the same walk and attach. +A runtime bind, such as the view's machine ID, goes through the same walk and +attach. `MappingTable` keeps a guest agent's account of a host mapping table: the mappings that the host hands the agent in `MAPS` parts once it is ready, after @@ -1136,16 +1137,16 @@ decode, holds no entry, does not continue the table, ends past the announcement, or maps the export's root, which an empty source names, as a `TableError::Refused` at the entry that it expects next, which changes nothing. It hands the part's mappings to the caller, which applies them beneath the root -of its choice, such as through a `Mapper` or a layered `Root`; the first that +of its choice, such as through a `Mapper`; the first that fails latches the table, which reports it as a `TableError::Failed` with its entry, and refuses every later part. The table is complete once every announced -mapping is applied, and none failed, and the root's teardown tears it down. +mapping is applied, and none failed, and the caller's teardown tears it down. Linux 6.8 or later. NVX's managed agent applies its host mapping table through a `MappingTable` and a mapper, beneath `/` in direct mode; the table refuses an empty source, since the export is an aggregate, whose root lists its children, and NVX's hosts, whose exports are all aggregates, send none. A layered `Root` -applies its live shares and mappings through a mapper. +mounts its live shares through a mapper. ### `nvx::guest::nvxs` @@ -1308,8 +1309,7 @@ export's `SourceRoot`, to map the host's mapping table from once the clone is ready, `proxy` the network's proxy endpoint, which the adapter tells its workloads as at a cold boot, and `publication` and `gateway`, from which its caller starts and supervises ingress. Its `teardown` closes the export's root, -then tears the root down with its view binds; `into_parts` returns -`(Root, MachineId)`, retaining every bind in the root. A switched root unmounts +then tears the root down with its view binds. A switched root unmounts nothing. A failure returns an `ApplyError`, `Export` or `Network` among them, after which nothing that it mounted or bound stays mounted unless the error reports the unmounts that @@ -1378,9 +1378,6 @@ shows read-only or without the mode's options in effect, since the kernel mounts one so, rather than fail, when its upper can't hold its work directory or its extended attributes. -The `Root` reports the first partition's bytes, its `Lower` layers, its -`UpperKind`, and its `OverlayMode`. - `Root::mount_share` and `Root::mount_aggregate`: virtio-fs live shares at targets in the root that it checks, against the targets that `protocol::share` reserves among others, with one aggregate per root, which it attaches through @@ -1392,13 +1389,11 @@ and a copy of each child's entry of the aggregate. `protocol::export`, one per root, mounted `nosuid,nodev` with the access of its `ExportMode` at `hostfs/root` in the runtime directory, out of the root, below a directory that only root may enter, whose root it returns as the `SourceRoot` of -the caller's mappings. - -`Root::map`: a caller's host mappings, through the same mapper. A share, a -child, the export, or a mapping that fails latches the root. +the caller's mappings, which the caller's own mapper applies. A share, a child, +or the export that fails latches the root. `Root::mount_runtime`: the runtime's file systems, which an init mounts once -for its VM, before the root's first live share or mapping, after it makes every +for its VM, before the root's first live share, after it makes every mount of the root's tree `nosuid,nodev`: procfs at `/proc`, sysfs at `/sys`, read-only, a tmpfs at `/dev` with an execution's device nodes and links, devpts at `/dev/pts`, and tmpfs at `/dev/shm` and `/run`, with an execution's options, @@ -1406,9 +1401,9 @@ then the time status's directory, when the caller names one, bound read-only at `/run/nvx/time`. It refuses a mount point that leads through a symbolic link or where something is mounted already, and, before it changes anything, a call while the root holds the runtime's mounts, whose `/dev` and `/dev/pts` a second -restriction would make `nodev`, or a live share or mapping, since the `/run` +restriction would make `nodev`, or a live share, since the `/run` tmpfs would hide one beneath `/run` from the teardown. A call that fails -unmounts what it mounted. A later share or mapping beneath `/run` is on that +unmounts what it mounted. A later share beneath `/run` is on that tmpfs, which the root's mapper owns, as a guest's mapper owns its `/tmp`. Each attachment is checked at its named target before it is recorded. A runtime mount whose attachment fails and which cannot be detached keeps its mount @@ -1423,7 +1418,7 @@ refused, and dropping the root unmounts nothing, while descriptors opened before the switch, such as a `TargetRoot` and a `SourceRoot`, still map. A move that fails leaves the root where it was. -`Root::teardown`: the view's binds, then the shares and mappings in reverse order, a runtime mount whose +`Root::teardown`: the view's binds, then the shares in reverse order, a runtime mount whose attachment failed and which could not be detached, then the mounts of an aggregate, of the export, and of the runtime, the later first, the overlay, the layers from the top down, and the upper, with every failed sync or unmount @@ -1444,9 +1439,8 @@ Hidden, unstable internal reference code of the guest's space, like `root`. hostname from the view's bytes. Image mode leaves `/etc/machine-id` untouched; generated mode writes a new UUID at `MACHINE_ID_FILE` in the runtime directory and binds it read-only over the image's ID. Optional DNS writes one server in -`RESOLVER_FILE` and binds it read-only over `/etc/resolv.conf`. The existing -`entry::MachineId` and hostname constants, and activation's file constants, -remain available through re-exports. +`RESOLVER_FILE` and binds it read-only over `/etc/resolv.conf`. +`nvx::guest::activation` re-exports both file constants. The root owns every bind, including an attachment left by a failed call. Teardown before the switch removes them; after the switch teardown refuses @@ -2554,7 +2548,7 @@ account of a host's mapping table. NVX's own guest space is internal reference code, hidden, unstable, and crate-private once nothing outside NVX uses it: the mapper of `mappings` mounts host mappings beneath the descriptors of the roots that its caller names, and `root` assembles a layered root, mounts live -shares and mappings in it through the mapper, and tears it down. +shares in it through the mapper, and tears it down. `nvx-managed-agent` opens its control terminal and runs direct workloads through nvx-init's plain-process core: each child resets its priority, drops its credentials and capabilities, and enters its working directory before exec. diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 501e4423a..d8685673f 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -380,19 +380,13 @@ variant nvx::guest::activation::ActivationState::Restricted variant nvx::guest::activation::ActivationState::Started struct nvx::guest::activation::Applied (hidden) method nvx::guest::activation::Applied::gateway (hidden) -method nvx::guest::activation::Applied::hostname (hidden) -method nvx::guest::activation::Applied::into_parts (hidden) -method nvx::guest::activation::Applied::machine_id (hidden) method nvx::guest::activation::Applied::proxy (hidden) method nvx::guest::activation::Applied::publication (hidden) -method nvx::guest::activation::Applied::resolver (hidden) method nvx::guest::activation::Applied::root (hidden) method nvx::guest::activation::Applied::root_mut (hidden) method nvx::guest::activation::Applied::take_export (hidden) method nvx::guest::activation::Applied::teardown (hidden) struct nvx::guest::activation::AppliedTeardownError (hidden) -method nvx::guest::activation::AppliedTeardownError::binds (hidden) -method nvx::guest::activation::AppliedTeardownError::root (hidden) enum nvx::guest::activation::ApplyError (hidden) variant nvx::guest::activation::ApplyError::Assembly (hidden) field nvx::guest::activation::ApplyError::Assembly::0 (hidden) @@ -401,9 +395,6 @@ field nvx::guest::activation::ApplyError::Export::error (hidden) field nvx::guest::activation::ApplyError::Export::teardown (hidden) variant nvx::guest::activation::ApplyError::Gpt (hidden) field nvx::guest::activation::ApplyError::Gpt::0 (hidden) -variant nvx::guest::activation::ApplyError::MachineId (hidden) -field nvx::guest::activation::ApplyError::MachineId::error (hidden) -field nvx::guest::activation::ApplyError::MachineId::teardown (hidden) variant nvx::guest::activation::ApplyError::Network (hidden) field nvx::guest::activation::ApplyError::Network::error (hidden) field nvx::guest::activation::ApplyError::Network::teardown (hidden) @@ -570,19 +561,13 @@ variant nvx::guest::clone::ActivationState::Restricted variant nvx::guest::clone::ActivationState::Started struct nvx::guest::clone::Applied (hidden) method nvx::guest::clone::Applied::gateway (hidden) -method nvx::guest::clone::Applied::hostname (hidden) -method nvx::guest::clone::Applied::into_parts (hidden) -method nvx::guest::clone::Applied::machine_id (hidden) method nvx::guest::clone::Applied::proxy (hidden) method nvx::guest::clone::Applied::publication (hidden) -method nvx::guest::clone::Applied::resolver (hidden) method nvx::guest::clone::Applied::root (hidden) method nvx::guest::clone::Applied::root_mut (hidden) method nvx::guest::clone::Applied::take_export (hidden) method nvx::guest::clone::Applied::teardown (hidden) struct nvx::guest::clone::AppliedTeardownError (hidden) -method nvx::guest::clone::AppliedTeardownError::binds (hidden) -method nvx::guest::clone::AppliedTeardownError::root (hidden) enum nvx::guest::clone::ApplyError (hidden) variant nvx::guest::clone::ApplyError::Assembly (hidden) field nvx::guest::clone::ApplyError::Assembly::0 (hidden) @@ -591,9 +576,6 @@ field nvx::guest::clone::ApplyError::Export::error (hidden) field nvx::guest::clone::ApplyError::Export::teardown (hidden) variant nvx::guest::clone::ApplyError::Gpt (hidden) field nvx::guest::clone::ApplyError::Gpt::0 (hidden) -variant nvx::guest::clone::ApplyError::MachineId (hidden) -field nvx::guest::clone::ApplyError::MachineId::error (hidden) -field nvx::guest::clone::ApplyError::MachineId::teardown (hidden) variant nvx::guest::clone::ApplyError::Network (hidden) field nvx::guest::clone::ApplyError::Network::error (hidden) field nvx::guest::clone::ApplyError::Network::teardown (hidden) @@ -738,12 +720,8 @@ variant nvx::guest::mappings::Kind::Directory variant nvx::guest::mappings::Kind::File const nvx::guest::mappings::MAX_PATH_BYTES struct nvx::guest::mappings::Mapper (hidden) -method nvx::guest::mappings::Mapper::apply (hidden) -method nvx::guest::mappings::Mapper::check (hidden) -method nvx::guest::mappings::Mapper::is_latched (hidden) method nvx::guest::mappings::Mapper::new (hidden) method nvx::guest::mappings::Mapper::targets (hidden) -method nvx::guest::mappings::Mapper::teardown (hidden) struct nvx::guest::mappings::Mapping method nvx::guest::mappings::Mapping::access method nvx::guest::mappings::Mapping::kind @@ -827,18 +805,13 @@ method nvx::guest::mappings::TeardownError::failures (hidden) struct nvx::guest::mappings::UnmountFailure (hidden) method nvx::guest::mappings::UnmountFailure::error (hidden) method nvx::guest::mappings::UnmountFailure::target (hidden) -fn nvx::guest::mappings::bind (hidden) mod nvx::guest::mappings::mapper (hidden) struct nvx::guest::mappings::mapper::ApplyError (hidden) method nvx::guest::mappings::mapper::ApplyError::entry (hidden) method nvx::guest::mappings::mapper::ApplyError::error (hidden) struct nvx::guest::mappings::mapper::Mapper (hidden) -method nvx::guest::mappings::mapper::Mapper::apply (hidden) -method nvx::guest::mappings::mapper::Mapper::check (hidden) -method nvx::guest::mappings::mapper::Mapper::is_latched (hidden) method nvx::guest::mappings::mapper::Mapper::new (hidden) method nvx::guest::mappings::mapper::Mapper::targets (hidden) -method nvx::guest::mappings::mapper::Mapper::teardown (hidden) enum nvx::guest::mappings::mapper::MappingError (hidden) variant nvx::guest::mappings::mapper::MappingError::Foreign (hidden) field nvx::guest::mappings::mapper::MappingError::Foreign::path (hidden) @@ -882,7 +855,6 @@ method nvx::guest::mappings::mapper::TeardownError::failures (hidden) struct nvx::guest::mappings::mapper::UnmountFailure (hidden) method nvx::guest::mappings::mapper::UnmountFailure::error (hidden) method nvx::guest::mappings::mapper::UnmountFailure::target (hidden) -fn nvx::guest::mappings::mapper::bind (hidden) mod nvx::guest::mmio_write (hidden) fn nvx::guest::mmio_write::main (hidden) mod nvx::guest::nvxs @@ -931,9 +903,6 @@ field nvx::guest::root::Assembly::runtime (hidden) field nvx::guest::root::Assembly::timeout (hidden) field nvx::guest::root::Assembly::upper (hidden) struct nvx::guest::root::AssemblyError (hidden) -method nvx::guest::root::AssemblyError::cleanup (hidden) -method nvx::guest::root::AssemblyError::error (hidden) -method nvx::guest::root::AssemblyError::into_parts (hidden) struct nvx::guest::root::Device (hidden) field nvx::guest::root::Device::mmio (hidden) field nvx::guest::root::Device::sectors (hidden) @@ -1032,10 +1001,6 @@ enum nvx::guest::root::Journal (hidden) variant nvx::guest::root::Journal::Allowed (hidden) variant nvx::guest::root::Journal::Forbidden (hidden) struct nvx::guest::root::Lower (hidden) -method nvx::guest::root::Lower::device (hidden) -method nvx::guest::root::Lower::mount (hidden) -method nvx::guest::root::Lower::partition (hidden) -method nvx::guest::root::Lower::path (hidden) const nvx::guest::root::MOUNT_OPTIONS_LIMIT (hidden) enum nvx::guest::root::Mount (hidden) variant nvx::guest::root::Mount::Aggregate (hidden) @@ -1066,18 +1031,12 @@ enum nvx::guest::root::Role (hidden) variant nvx::guest::root::Role::Distro (hidden) variant nvx::guest::root::Role::Scratch (hidden) struct nvx::guest::root::Root (hidden) -method nvx::guest::root::Root::first_partition (hidden) -method nvx::guest::root::Root::is_latched (hidden) -method nvx::guest::root::Root::lowers (hidden) -method nvx::guest::root::Root::map (hidden) method nvx::guest::root::Root::mount_aggregate (hidden) method nvx::guest::root::Root::mount_export (hidden) method nvx::guest::root::Root::mount_share (hidden) -method nvx::guest::root::Root::overlay (hidden) method nvx::guest::root::Root::path (hidden) method nvx::guest::root::Root::runtime (hidden) method nvx::guest::root::Root::teardown (hidden) -method nvx::guest::root::Root::upper (hidden) enum nvx::guest::root::RuntimeMounts (hidden) variant nvx::guest::root::RuntimeMounts::Dev (hidden) variant nvx::guest::root::RuntimeMounts::DevPts (hidden) @@ -1116,11 +1075,7 @@ variant nvx::guest::root::ShareRefusal::SecondExport (hidden) variant nvx::guest::root::ShareRefusal::Tag (hidden) field nvx::guest::root::ShareRefusal::Tag::0 (hidden) struct nvx::guest::root::TeardownError (hidden) -method nvx::guest::root::TeardownError::failures (hidden) struct nvx::guest::root::Unmount (hidden) -method nvx::guest::root::Unmount::error (hidden) -method nvx::guest::root::Unmount::mount (hidden) -method nvx::guest::root::Unmount::path (hidden) enum nvx::guest::root::Upper (hidden) variant nvx::guest::root::Upper::Scratch (hidden) field nvx::guest::root::Upper::Scratch::device (hidden) @@ -1280,9 +1235,7 @@ fn nvx::guest::time::main (hidden) mod nvx::guest::time_probe (hidden) fn nvx::guest::time_probe::main (hidden) mod nvx::guest::view (hidden) -const nvx::guest::view::DEFAULT_HOSTNAME (hidden) const nvx::guest::view::MACHINE_ID_FILE (hidden) -const nvx::guest::view::MAX_HOSTNAME_BYTES (hidden) enum nvx::guest::view::MachineId (hidden) variant nvx::guest::view::MachineId::Generated (hidden) field nvx::guest::view::MachineId::Generated::0 (hidden) diff --git a/nvx-rs/src/guest/activation.rs b/nvx-rs/src/guest/activation.rs index 16c7c1e11..763ce71c9 100644 --- a/nvx-rs/src/guest/activation.rs +++ b/nvx-rs/src/guest/activation.rs @@ -148,10 +148,9 @@ //! the adapter maps the host's mapping table once the clone is ready, the network's proxy //! endpoint, which the adapter tells its workloads as at a cold boot, and the publication and //! gateway with which the caller starts and supervises ingress. The `teardown` of the `Applied` -//! closes the export's root, then tears the root down with its view binds. Its `into_parts` -//! returns the root and machine ID, with every bind still owned by the root. An -//! application that fails returns an `ApplyError` instead, after which nothing that it mounted or -//! bound stays mounted, unless the error holds the unmounts that failed. +//! closes the export's root, then tears the root down with its view binds. An application that +//! fails returns an `ApplyError` instead, after which nothing that it mounted or bound stays +//! mounted, unless the error holds the unmounts that failed. //! //! # Example //! diff --git a/nvx-rs/src/guest/activation/state/apply.rs b/nvx-rs/src/guest/activation/state/apply.rs index 63cf69e8e..1a495d7c2 100644 --- a/nvx-rs/src/guest/activation/state/apply.rs +++ b/nvx-rs/src/guest/activation/state/apply.rs @@ -9,14 +9,14 @@ mod tests; use std::fmt; use std::io; use std::net::{IpAddr, Ipv4Addr}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::time::{Duration, Instant}; #[cfg(doc)] use super::Activation; use crate::activation::{self, ActivationRecord, Configuration, Publication}; use crate::gpt::Expected; -use crate::guest::mappings::{self, Access, SourceRoot}; +use crate::guest::mappings::{Access, SourceRoot}; use crate::guest::root::{self, Assembly, AssemblyError, Device, Root, TeardownError}; #[cfg(doc)] use crate::guest::snapshot::Devices; @@ -55,12 +55,6 @@ pub struct Applied { export: Option, /// The root, which also owns the view's binds. root: Root, - /// The VM-wide hostname taken from the record. - hostname: Vec, - /// The machine ID that the view applied. - machine_id: MachineId, - /// The resolver file, when the record names a DNS server. - resolver: Option, /// The proxy endpoint, which the caller tells its workloads. proxy: Option, /// Whether the record publishes listeners. @@ -75,26 +69,11 @@ impl Applied { &self.root } - /// Returns the assembled root to mount runtime filesystems, shares, and mappings. + /// Returns the assembled root to mount runtime filesystems and shares in, and to switch into. pub fn root_mut(&mut self) -> &mut Root { &mut self.root } - /// Returns the VM-wide hostname taken from the record. - pub fn hostname(&self) -> &[u8] { - &self.hostname - } - - /// Returns the machine ID that the root's view applied. - pub fn machine_id(&self) -> &MachineId { - &self.machine_id - } - - /// Returns the runtime resolver file, or `None` when the image's resolver remains in use. - pub fn resolver(&self) -> Option<&Path> { - self.resolver.as_deref() - } - /// Returns the proxy endpoint for the caller to tell its workloads. pub fn proxy(&self) -> Option<&str> { self.proxy.as_deref() @@ -121,48 +100,21 @@ impl Applied { pub fn teardown(self) -> Result<(), AppliedTeardownError> { teardown_with(&mut LiveSystem, self.export, self.root) } - - /// Returns the root and its machine ID, closing an export descriptor not already taken. - /// - /// The root retains the view's binds. No separate owner can unmount them after its switch. - pub fn into_parts(self) -> (Root, MachineId) { - drop(self.export); - (self.root, self.machine_id) - } } -/// Why an applied root's teardown failed. -/// -/// View-bind failures now belong to the root's teardown. The separate bind-failure accessor -/// remains for compatibility with callers of the earlier application interface. +/// Why an applied root's teardown failed: the root's teardown, which includes its view binds. #[doc(hidden)] #[derive(Debug)] pub struct AppliedTeardownError { - /// A legacy failure of independently owned runtime binds. - binds: Option, /// The root's teardown failure, including its view binds. root: Option, } -impl AppliedTeardownError { - /// Returns a legacy independently owned bind failure; new teardowns report through `root`. - pub fn binds(&self) -> Option<&mappings::TeardownError> { - self.binds.as_ref() - } - - /// Returns the root's teardown failure, including its view binds. - pub fn root(&self) -> Option<&TeardownError> { - self.root.as_ref() - } -} - impl fmt::Display for AppliedTeardownError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match (&self.binds, &self.root) { - (Some(binds), Some(root)) => write!(formatter, "{binds}; {root}"), - (Some(binds), None) => binds.fmt(formatter), - (None, Some(root)) => root.fmt(formatter), - (None, None) => Ok(()), + match &self.root { + Some(root) => root.fmt(formatter), + None => Ok(()), } } } @@ -189,9 +141,6 @@ pub fn apply( Ok(Applied { export: activated.export, root: activated.root, - hostname: activated.hostname, - machine_id: activated.machine_id, - resolver: activated.resolver, proxy: activated.proxy, publication: activated.publication, gateway: activated.gateway, @@ -207,13 +156,6 @@ pub enum ApplyError { Gpt(root::Error), /// Assembly failed, including a refusal of a non-fresh upper. Assembly(AssemblyError), - /// A legacy separately generated machine ID could not be written. - MachineId { - /// Why generation failed. - error: io::Error, - /// Any root teardown failure. - teardown: Option, - }, /// The VM-wide view could not be prepared or its machine ID applied. View { /// The failure reported by [`view::apply`]. @@ -249,13 +191,6 @@ impl fmt::Display for ApplyError { Self::Assembly(error) => { return write!(formatter, "the root's assembly failed: {error}"); } - Self::MachineId { error, teardown } => { - write!( - formatter, - "the workload's machine ID cannot be generated: {error}" - )?; - teardown - } Self::View { error, teardown } => { write!(formatter, "the workload's view cannot be set: {error}")?; teardown @@ -282,7 +217,6 @@ impl std::error::Error for ApplyError { Self::Gpt(error) | Self::Export { error, .. } => Some(error), Self::Assembly(error) => Some(error), Self::View { error, .. } => Some(error), - Self::MachineId { error, .. } => Some(error), Self::Network { error, .. } => Some(error), } } @@ -422,12 +356,6 @@ struct Activated { root: R, /// The export descriptor, if any. export: Option, - /// The record's VM-wide hostname. - hostname: Vec, - /// The machine ID applied by the view. - machine_id: MachineId, - /// The resolver file, if DNS was supplied. - resolver: Option, /// The record's proxy endpoint. proxy: Option, /// The record's publication, or unpublished without a network. @@ -478,19 +406,13 @@ fn apply_with( }); } let dns = network.and_then(|network| network.dns); - let machine_id = match system.view(&mut root, &configuration.view, dns) { - Ok(machine_id) => machine_id, - Err(error) => { - let teardown = discard(system, export, root); - return Err(view_failure(error, teardown)); - } - }; + if let Err(error) = system.view(&mut root, &configuration.view, dns) { + let teardown = discard(system, export, root); + return Err(view_failure(error, teardown)); + } Ok(Activated { root, export, - hostname: configuration.view.hostname.clone(), - machine_id, - resolver: dns.map(|_| template.runtime.join(RESOLVER_FILE)), proxy: network.and_then(|network| network.proxy.clone()), publication, gateway: template.gateway, @@ -527,10 +449,7 @@ fn teardown_with( ) -> Result<(), AppliedTeardownError> { match discard(system, export, root) { None => Ok(()), - Some(root) => Err(AppliedTeardownError { - binds: None, - root: Some(root), - }), + Some(root) => Err(AppliedTeardownError { root: Some(root) }), } } diff --git a/nvx-rs/src/guest/activation/state/apply/tests.rs b/nvx-rs/src/guest/activation/state/apply/tests.rs index 149435110..11d83e932 100644 --- a/nvx-rs/src/guest/activation/state/apply/tests.rs +++ b/nvx-rs/src/guest/activation/state/apply/tests.rs @@ -2,6 +2,7 @@ //! The seam has no process or admission operation. use std::error::Error as _; +use std::path::Path; use super::*; use crate::activation::{ @@ -11,6 +12,7 @@ use crate::activation::{ use crate::guest::activation::{ Activation, ActivationAction, ActivationCause, ActivationInput, ActivationState, }; +use crate::guest::root::tests::assembly_failure; use crate::guest::sys::Errno; use crate::protocol::root::{DISTRO_MMIO_BASE, SCRATCH_MMIO_BASE}; use crate::uuid::Uuid; @@ -336,14 +338,6 @@ fn a_valid_activate_activates_the_root_and_the_workload_once_before_ready() { Effect::View(record.configuration().view.clone(), Some(DNS)), ]; assert_eq!(system.effects, expected); - assert_eq!( - applied.machine_id, - MachineId::Generated(Path::new(RUNTIME).join(MACHINE_ID_FILE)) - ); - assert_eq!( - applied.resolver, - Some(Path::new(RUNTIME).join(RESOLVER_FILE)) - ); assert_eq!(applied.proxy.as_deref(), Some(PROXY)); assert_eq!(applied.publication, Publication::Published); assert_eq!(applied.gateway, Some(GATEWAY)); @@ -583,7 +577,7 @@ fn a_failed_assembly_is_final_and_reads_and_tears_down_nothing() { }; let error = apply_with(&mut system, &accepted(), &template(), None).unwrap_err(); assert!(matches!(&error, ApplyError::Assembly(error) if matches!( - error.error(), root::Error::Runtime { path } if path == Path::new("run/nvx") + assembly_failure(error), root::Error::Runtime { path } if path == Path::new("run/nvx") ))); let failure = activation.complete(now, Err(error.into())).unwrap_err(); assert!(matches!(failure.cause(), ActivationCause::Failed(_))); @@ -596,7 +590,7 @@ fn a_failed_assembly_is_final_and_reads_and_tears_down_nothing() { } #[test] -fn the_record_s_view_is_applied_once_with_its_own_dns_and_returned_machine_id() { +fn the_record_s_view_is_applied_once_with_its_own_dns() { let ipv6 = IpAddr::V6("fd00::53".parse().unwrap()); for mode in [MachineIdMode::Image, MachineIdMode::Generated] { for dns in [None, Some(DNS), Some(ipv6)] { @@ -617,18 +611,6 @@ fn the_record_s_view_is_applied_once_with_its_own_dns_and_returned_machine_id() Effect::View(configuration.view, dns), ] ); - assert_eq!( - applied.machine_id, - match mode { - MachineIdMode::Image => MachineId::Image, - MachineIdMode::Generated => - MachineId::Generated(Path::new(RUNTIME).join(MACHINE_ID_FILE)), - } - ); - assert_eq!( - applied.resolver, - dns.map(|_| Path::new(RUNTIME).join(RESOLVER_FILE)) - ); assert_eq!(applied.proxy.as_deref(), Some(PROXY)); } } @@ -797,7 +779,10 @@ fn the_application_refuses_without_privileges_what_it_cannot_assemble() { let Err(ApplyError::Assembly(error)) = apply(&accepted(), &relative, None) else { panic!("a relative runtime directory was assembled") }; - assert!(matches!(error.error(), root::Error::Runtime { path } if path == Path::new("run/nvx"))); + assert!(matches!( + assembly_failure(&error), + root::Error::Runtime { path } if path == Path::new("run/nvx") + )); let disk = clone_record(Configuration { image: activation::Image::Gpt { disk_guid: None, @@ -855,19 +840,3 @@ impl ApplyError { view_failure(failure, None) } } - -impl Applied { - /// Wraps an assembled root to exercise the ownership transfer in the root's unit tests. - pub(crate) fn from_test_root(root: Root, machine_id: MachineId) -> Self { - Self { - export: None, - root, - hostname: HOSTNAME.to_vec(), - machine_id, - resolver: None, - proxy: None, - publication: Publication::Unpublished, - gateway: None, - } - } -} diff --git a/nvx-rs/src/guest/init/boot.rs b/nvx-rs/src/guest/init/boot.rs index 149c2966a..372412102 100644 --- a/nvx-rs/src/guest/init/boot.rs +++ b/nvx-rs/src/guest/init/boot.rs @@ -534,9 +534,7 @@ fn class_status(failure: &ActivationFailure) -> u8 { Some(ApplyError::Gpt(_) | ApplyError::Assembly(_)) => { Some(status::ACTIVATION_ROOT) } - Some(ApplyError::MachineId { .. } | ApplyError::View { .. }) => { - Some(status::ACTIVATION_WORKLOAD) - } + Some(ApplyError::View { .. }) => Some(status::ACTIVATION_WORKLOAD), Some(ApplyError::Export { .. }) => Some(status::ACTIVATION_EXPORT), Some(ApplyError::Network { .. }) => Some(status::ACTIVATION_NETWORK), None => error diff --git a/nvx-rs/src/guest/init/boot/tests.rs b/nvx-rs/src/guest/init/boot/tests.rs index 2fd8139cd..5fac93a07 100644 --- a/nvx-rs/src/guest/init/boot/tests.rs +++ b/nvx-rs/src/guest/init/boot/tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::ErrorKind; use crate::activation::MachineIdMode; +use crate::guest::mappings::SystemKernel; use crate::guest::{account as accounts, sys}; const ROOT: &str = "nvx_layer=distro,0xd0003000,erofs nvx_overlay_upper=ramfs"; @@ -414,14 +415,6 @@ fn a_failed_activation_ends_the_vm_with_the_status_of_its_class() { applying(ApplyError::Gpt(missing())), ACTIVATION_ROOT, ), - ( - "a machine ID", - applying(ApplyError::MachineId { - error: std::io::Error::other("no machine ID"), - teardown: None, - }), - ACTIVATION_WORKLOAD, - ), ( "a view", applying(ApplyError::View { @@ -845,7 +838,7 @@ fn layered_cold_boot_maps_through_handles_opened_before_its_only_switch() { boot.table .apply(part, |batch| { boot.mapper - .apply(export, batch) + .apply_with(&mut SystemKernel, export, batch) .map_err(|error| (error.entry(), error)) }) .unwrap(); diff --git a/nvx-rs/src/guest/mappings.rs b/nvx-rs/src/guest/mappings.rs index d7ac38c05..d691320a6 100644 --- a/nvx-rs/src/guest/mappings.rs +++ b/nvx-rs/src/guest/mappings.rs @@ -19,9 +19,9 @@ //! target, and its access. A [`MappingTable`] keeps the agent's account of the table: it checks //! each part against what the host announced and against the entries that it applied, hands the //! part's mappings to the caller, which applies them beneath the root of its choice, such as -//! through a mapper or a layered root, and latches at the first that fails, which it reports with -//! its entry in the table, so that the agent starts no workload on a partial table. The root keeps -//! the mappings until its teardown. +//! through a mapper, and latches at the first that fails, which it reports with its entry in the +//! table, so that the agent starts no workload on a partial table. The caller keeps the mappings +//! until its teardown. //! //! NVX's managed agent applies its host mapping table through one, and through a mapper beneath //! its own root, `/`, in direct mode, from its mount of the host's export, an aggregate, whose @@ -43,7 +43,7 @@ pub(crate) use self::kernel::{Kernel, SystemKernel}; #[doc(hidden)] pub use self::mapper::{ ApplyError, Mapper, MappingError, Reserved, SourceRoot, Step, TargetRoot, TeardownError, - UnmountFailure, bind, + UnmountFailure, }; pub(crate) use self::mapper::{Mapped, unbind_with}; pub use self::table::{MappingTable, Refusal, TableError}; diff --git a/nvx-rs/src/guest/mappings/mapper.rs b/nvx-rs/src/guest/mappings/mapper.rs index 2e3dde9eb..3e6cb3725 100644 --- a/nvx-rs/src/guest/mappings/mapper.rs +++ b/nvx-rs/src/guest/mappings/mapper.rs @@ -6,10 +6,10 @@ //! beside the mapping values and the host mapping table, which stay public. //! //! A [`Mapper`] applies [`Mapping`]s beneath one [`TargetRoot`], in order, from a [`SourceRoot`], -//! tracks them, and unmounts them at its [`teardown`](Mapper::teardown). [`bind`] applies one -//! runtime bind, such as a generated machine ID, a resolver file, or the runtime's time status, -//! through the same walk and attach, without the checks of a host mapping's target and without -//! tracking. +//! tracks them, and unmounts them, in reverse order, at the teardown of the layered root that +//! owns it. A runtime bind, such as a generated machine ID, a resolver file, or the runtime's time +//! status, goes through the same walk and attach, without the checks of a host mapping's target +//! and without tracking. //! //! # Walk //! @@ -86,40 +86,23 @@ //! //! # Order and failure //! -//! [`Mapper::apply`] applies a batch of mappings in order. Zero mappings do no work. The first +//! A mapper applies a batch of mappings in order. Zero mappings do no work. The first //! mapping that is refused or fails is reported with its index; the mappings before it stay //! mapped, and the mapper refuses every later one, in this batch or another: the failure latches //! for the mapper's lifetime, so that its caller starts nothing on a partial table. //! //! # Teardown //! -//! [`Mapper::teardown`] first detaches the mapping that a rename moved, if its attach failed to -//! detach it, through the descriptor that the mapper kept, then unmounts the mappings in reverse -//! order. It opens each target again from the root, checks that the topmost mount there is the -//! mapping's, and unmounts it by its name in its parent directory, whose descriptor it opened -//! from the root, without following a link there and without detaching a busy mount; this needs -//! procfs at `/proc`. It attempts each, whatever failed before, and reports every failure. A -//! mapper that is dropped leaves its mappings mounted. +//! At the teardown of the layered root that owns a mapper, `Mapper::unmount_with` first detaches +//! the mapping that a rename moved, if its attach failed to detach it, through the descriptor that +//! the mapper kept, then unmounts the mappings in reverse order. It opens each target again from +//! the root, checks that the topmost mount there is the mapping's, and unmounts it by its name in +//! its parent directory, whose descriptor it opened from the root, without following a link there +//! and without detaching a busy mount; this needs procfs at `/proc`. It attempts each, whatever +//! failed before, and reports every failure. A mapper that is dropped leaves its mappings mounted. //! -//! A layered [`Root`](crate::guest::root::Root) mounts its live shares, and the mappings that its -//! caller applies, through one, which its teardown unmounts first. -//! -//! ```no_run -//! use std::path::Path; -//! -//! use nvx::guest::mappings::{Access, Mapper, Mapping, Reserved, SourceRoot, TargetRoot}; -//! -//! # fn main() -> Result<(), Box> { -//! let root = TargetRoot::open(Path::new("/newroot"))?; -//! let mut mapper = Mapper::new(root, Reserved::container_root()); -//! let export = SourceRoot::open(Path::new("/run/export"))?; -//! // Mounts the export's 0/workspace at /newroot/workspace, read-write, nosuid, and nodev. -//! let workspace = Mapping::new("0/workspace", "/workspace", Access::ReadWrite)?; -//! mapper.apply(&export, &[workspace])?; -//! mapper.teardown()?; -//! # Ok(()) -//! # } -//! ``` +//! A layered [`Root`](crate::guest::root::Root) mounts its live shares through one, which its +//! teardown unmounts first. #[cfg(test)] mod tests; @@ -277,7 +260,7 @@ impl TargetRoot { } } -/// The root beneath which a [`Mapper`] or [`bind`] opens the sources of mappings, such as the +/// The root beneath which a [`Mapper`] or a runtime bind opens the sources of mappings, such as the /// guest's mount of the host's export: a descriptor of a directory. #[derive(Debug)] pub struct SourceRoot { @@ -380,8 +363,8 @@ pub enum MappingError { /// mount at the target since then. Moved { /// Whether the mapping stays mounted, wherever it is: because detaching it failed too, - /// in which case a mapper's teardown detaches it, or because a teardown does not unmount - /// what it does not find at its target. + /// in which case the mapper's `unmount_with` detaches it, or because a teardown does not + /// unmount what it does not find at its target. mounted: bool, }, /// A system call failed at a step. @@ -558,8 +541,8 @@ impl fmt::Display for TeardownError { impl std::error::Error for TeardownError {} -/// A mapping that a mapper attached and tracks, or a runtime bind that [`bind_with`] made, which -/// [`unbind_with`] unmounts. +/// A mapping that a mapper attached and tracks, or a runtime bind that [`bind_retained_with`] made, +/// which [`unbind_with`] unmounts. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Mapped { /// The target, relative to the root. @@ -607,57 +590,20 @@ impl Mapper { } } - /// Applies `mappings` beneath the root, in order, from `source`, as the [module](self) - /// describes, and tracks them. It needs the privilege to mount. - /// - /// The first mapping that is refused or fails is reported with its index, after the mappings - /// before it, which stay mapped, and the mapper refuses every later mapping from then on. - /// Zero mappings do no work. A mapping that a rename moved, and that the mapper failed to - /// detach, [`MappingError::Moved`] with `mounted`, is not tracked, but the teardown detaches - /// it first. - pub fn apply(&mut self, source: &SourceRoot, mappings: &[Mapping]) -> Result<(), ApplyError> { - self.apply_with(&mut SystemKernel, source, mappings) - } - - /// Checks the target of `mapping` as [`apply`](Self::apply) does before it opens anything: - /// that it is neither reserved nor overlaps an earlier mapping. - pub fn check(&self, mapping: &Mapping) -> Result<(), MappingError> { - self.check_target(mapping.target()) - } - - /// Returns whether a mapping failed, after which the mapper refuses every later one. - pub fn is_latched(&self) -> bool { - self.latched - } - /// Returns the targets of the mappings that the mapper tracks, in the order in which it /// applied them, relative to the root. pub fn targets(&self) -> impl ExactSizeIterator { self.mapped.iter().map(|mapped| mapped.target.as_path()) } - /// Unmounts the mappings in reverse order, as the [module](self) describes, and reports every - /// one that it failed to unmount, which stays mounted. It first detaches the mapping that a - /// rename moved and that the mapper failed to detach, if any. - pub fn teardown(self) -> Result<(), TeardownError> { - self.teardown_with(&mut SystemKernel) - } - - /// Implements [`teardown`](Self::teardown) with `kernel`. - pub(crate) fn teardown_with(mut self, kernel: &mut dyn Kernel) -> Result<(), TeardownError> { - let failures: Vec = self - .unmount_with(kernel) - .into_iter() - .map(|(_, failure)| failure) - .collect(); - if failures.is_empty() { - Ok(()) - } else { - Err(TeardownError { failures }) - } - } - - /// Implements [`apply`](Self::apply) with `kernel`. + /// Applies `mappings` beneath the root, in order, from `source`, with `kernel`, as the + /// [module](self) describes, and tracks them. It needs the privilege to mount. + /// + /// The first mapping that is refused or fails is reported with its index, after the mappings + /// before it, which stay mapped, and the mapper refuses every later mapping from then on. + /// Zero mappings do no work. A mapping that a rename moved, and that the mapper failed to + /// detach, [`MappingError::Moved`] with `mounted`, is not tracked, but the teardown detaches + /// it first. pub(crate) fn apply_with( &mut self, kernel: &mut dyn Kernel, @@ -681,8 +627,8 @@ impl Mapper { /// mounted where only it can reach, onto `target`, a path relative to the root, as the mapper /// attaches a directory's mapping: with `nosuid`, `nodev`, and `access` set while the mount is /// still where only the caller reaches it, and after the same checks. The mapper then tracks - /// it as a mapping, or, as [`apply`](Self::apply) does, detaches it at the teardown if a - /// rename moved it and detaching it failed. + /// it as a mapping, or, as `apply_with` does, detaches it at the teardown if a rename moved it + /// and detaching it failed. pub(crate) fn move_mount_with( &mut self, kernel: &mut dyn Kernel, @@ -700,10 +646,10 @@ impl Mapper { } /// Detaches the mapping that a rename moved, if the mapper failed to detach it, then unmounts - /// the mappings in reverse order with `kernel`, forgets them all, and returns, in the order of - /// the attempts, each that it failed to unmount, with its index among the mapper's attaches - /// that left a mount: the moved mapping's, which the mapper attached last, is the number of - /// its mappings. + /// the mappings in reverse order with `kernel`, as the [module](self) describes, forgets them + /// all, and returns, in the order of the attempts, each that it failed to unmount, which stays + /// mounted, with its index among the mapper's attaches that left a mount: the moved mapping's, + /// which the mapper attached last, is the number of its mappings. pub(crate) fn unmount_with(&mut self, kernel: &mut dyn Kernel) -> Vec<(usize, UnmountFailure)> { let mapped = std::mem::take(&mut self.mapped); let mut failures = Vec::new(); @@ -723,7 +669,8 @@ impl Mapper { failures } - /// Checks the relative `target` as [`check`](Self::check) does. + /// Checks the relative `target` before an attach opens anything: that it is neither reserved + /// nor overlaps an earlier mapping. fn check_target(&self, target: &Path) -> Result<(), MappingError> { let target = bytes(target); if self.reserved.holds(target) { @@ -782,28 +729,15 @@ impl Mapper { } } -/// Binds the source of `mapping`, beneath `source`, at its target beneath `root`, with the walk, -/// the creation, the checks of kinds and mounts, and the attach of a [`Mapper`]'s mapping, as the -/// [module](self) describes, but without the checks of reserved targets and overlaps, which a -/// runtime's own binds, such as a generated machine ID over `/etc/machine-id`, would fail, and -/// without tracking it: it lasts as long as the mount namespace, or until its caller unmounts it. -/// So does a bind that a rename moved and that it failed to detach, wherever it went, which it -/// reports as [`MappingError::Moved`] with `mounted`. It needs the privilege to mount. -pub fn bind(root: &TargetRoot, source: &SourceRoot, mapping: &Mapping) -> Result<(), MappingError> { - bind_with(&mut SystemKernel, root, source, mapping).map(drop) -} - -/// Implements [`bind`] with `kernel`, and returns the bind, which [`unbind_with`] unmounts. -pub(crate) fn bind_with( - kernel: &mut dyn Kernel, - root: &TargetRoot, - source: &SourceRoot, - mapping: &Mapping, -) -> Result { - bind_retained_with(kernel, root, source, mapping).map_err(|failure| failure.error) -} - -/// Implements [`bind_with`], retaining a moved mount that could not be detached for its caller. +/// Binds the source of `mapping`, beneath `source`, at its target beneath `root`, with `kernel`, +/// with the walk, the creation, the checks of kinds and mounts, and the attach of a [`Mapper`]'s +/// mapping, as the [module](self) describes, but without the checks of reserved targets and +/// overlaps, which a runtime's own binds, such as a generated machine ID over `/etc/machine-id`, +/// would fail, and without tracking it: it returns the bind, which [`unbind_with`] unmounts, and +/// which otherwise lasts as long as the mount namespace. A bind that a rename moved and that it +/// failed to detach stays mounted wherever it went, which it reports as [`MappingError::Moved`] +/// with `mounted`, with the descriptor of the bind's mount, for its caller to detach. It needs the +/// privilege to mount. pub(crate) fn bind_retained_with( kernel: &mut dyn Kernel, root: &TargetRoot, @@ -825,10 +759,11 @@ pub(crate) fn bind_retained_with( }) } -/// Unmounts `binds`, which [`bind_with`] made beneath `root`, in reverse order, with `kernel`, as a -/// mapper's teardown unmounts its mappings: each that is the topmost mount at its target, by its -/// name in its parent directory, without detaching a busy one. It attempts each, whatever failed -/// before, and reports every one that it failed to unmount, which stays mounted. +/// Unmounts `binds`, which [`bind_retained_with`] made beneath `root`, in reverse order, with +/// `kernel`, as [`Mapper::unmount_with`] unmounts a mapper's mappings: each that is the topmost +/// mount at its target, by its name in its parent directory, without detaching a busy one. It +/// attempts each, whatever failed before, and reports every one that it failed to unmount, which +/// stays mounted. pub(crate) fn unbind_with( kernel: &mut dyn Kernel, root: &TargetRoot, diff --git a/nvx-rs/src/guest/mappings/mapper/tests.rs b/nvx-rs/src/guest/mappings/mapper/tests.rs index 1300ca16c..1f279dacc 100644 --- a/nvx-rs/src/guest/mappings/mapper/tests.rs +++ b/nvx-rs/src/guest/mappings/mapper/tests.rs @@ -94,7 +94,7 @@ impl Fixture { let mut mapper = self.mapper(reserved); let error = self.apply(&mut mapper, &[mapping]).unwrap_err(); assert_eq!(error.entry(), 0); - assert!(mapper.is_latched()); + assert!(mapper.latched); error.error().clone() } } @@ -162,7 +162,7 @@ fn mappings_are_restricted_copies_attached_in_order() { for target in ["root/data/ro", "root/data/rw", "root/conf/app.conf"] { assert!(fixture.kernel.mount_at(&fixture.path(target)).is_some()); } - assert!(!mapper.is_latched()); + assert!(!mapper.latched); } #[test] @@ -430,7 +430,8 @@ fn targets_that_would_shadow_a_mapping_or_a_mount_are_refused() { .apply(&mut mapper, &[mapping("dir", "/data/x", Access::ReadOnly)]) .unwrap(); // The checks alone neither open anything nor latch. - let check = |target: &str| mapper.check(&mapping("dir", target, Access::ReadOnly)); + let check = + |target: &str| mapper.check_target(mapping("dir", target, Access::ReadOnly).target()); let overlap = MappingError::Overlap { mapped: "/data/x".into(), }; @@ -438,13 +439,13 @@ fn targets_that_would_shadow_a_mapping_or_a_mount_are_refused() { assert_eq!(check("/data"), Err(overlap.clone())); assert_eq!(check("/data/x/y"), Ok(())); assert_eq!(check("/data/xy"), Ok(())); - assert!(!mapper.is_latched()); + assert!(!mapper.latched); let error = fixture .apply(&mut mapper, &[mapping("dir", "/data", Access::ReadOnly)]) .unwrap_err(); assert_eq!(error.error(), &overlap); assert_eq!(error.error().raw_os_error(), Errno::EBUSY as i32); - assert!(mapper.is_latched()); + assert!(mapper.latched); // The root of a mount that the root does not own. fs::create_dir(fixture.path("root/mounted")).unwrap(); @@ -491,7 +492,7 @@ fn a_mapping_covers_the_root_of_a_mount_that_the_root_owns() { ); assert!(!tmp.join("new").exists()); // The teardown uncovers it. - mapper.teardown_with(&mut fixture.kernel()).unwrap(); + assert_eq!(mapper.unmount_with(&mut fixture.kernel()), Vec::new()); assert_eq!(fixture.kernel.mount_at(&tmp), Some(tmpfs)); } @@ -499,7 +500,8 @@ fn a_mapping_covers_the_root_of_a_mount_that_the_root_owns() { fn reserved_targets_and_their_ancestors_are_refused() { let fixture = Fixture::new(); let check = |reserved: Reserved, target: &str| { - Mapper::new(fixture.root(), reserved).check(&mapping("dir", target, Access::ReadOnly)) + Mapper::new(fixture.root(), reserved) + .check_target(mapping("dir", target, Access::ReadOnly).target()) }; // Every mapper reserves the runtime's time tree, and the ancestors that would shadow it. for target in ["/run", "/run/nvx", "/run/nvx/time", "/run/nvx/time/status"] { @@ -606,7 +608,7 @@ fn the_first_failure_latches_the_mapper() { assert!(fixture.kernel.mount_at(&fixture.path("root/one")).is_some()); assert!(!fixture.path("root/two").exists()); assert!(!fixture.path("root/three").exists()); - assert!(mapper.is_latched()); + assert!(mapper.latched); let calls = fixture.kernel.calls(); let error = fixture .apply( @@ -703,7 +705,7 @@ fn a_mapping_that_a_rename_moved_is_detached_and_refused() { .apply(&mut mapper, &[mapping("dir", "/data/x", Access::ReadOnly)]) .unwrap_err(); assert_eq!(error.entry(), 0); - assert!(mapper.is_latched()); + assert!(mapper.latched); assert_eq!( error.error(), &MappingError::Moved { @@ -728,7 +730,7 @@ fn a_mapping_that_a_rename_moved_is_detached_and_refused() { // it is. assert_eq!(mapper.targets().len(), 0); fixture.kernel.restore(Operation::Detach, &moved); - mapper.teardown_with(&mut fixture.kernel()).unwrap(); + assert_eq!(mapper.unmount_with(&mut fixture.kernel()), Vec::new()); assert_eq!(fixture.kernel.mount_at(&moved), None); let detaches = fixture .kernel @@ -871,20 +873,29 @@ fn a_teardown_unmounts_in_reverse_order_and_reports_each_failure() { .unwrap(); fixture.kernel.add_mount(fixture.path("root/covered")); fs::rename(fixture.path("root/renamed"), fixture.path("root/elsewhere")).unwrap(); - let error = mapper.teardown_with(&mut fixture.kernel()).unwrap_err(); + let failures = mapper.unmount_with(&mut fixture.kernel()); assert_eq!( - error.failures(), + failures, [ - UnmountFailure { - target: "/renamed".into(), - error: failed(Step::Locate, Errno::ENOENT), - }, - UnmountFailure { - target: "/covered".into(), - error: MappingError::Moved { mounted: true }, - }, + ( + 1, + UnmountFailure { + target: "/renamed".into(), + error: failed(Step::Locate, Errno::ENOENT), + } + ), + ( + 0, + UnmountFailure { + target: "/covered".into(), + error: MappingError::Moved { mounted: true }, + } + ), ] ); + let error = TeardownError { + failures: failures.into_iter().map(|(_, failure)| failure).collect(), + }; assert_eq!( error.to_string(), format!( @@ -907,7 +918,10 @@ fn a_runtime_bind_skips_the_checks_of_a_host_mapping_but_not_the_walk() { fs::write(fixture.path("root/etc/machine-id"), b"image\n").unwrap(); let root = fixture.root(); let source = fixture.source(); - let bind = |mapping: Mapping| bind_with(&mut fixture.kernel(), &root, &source, &mapping); + let bind = |mapping: Mapping| { + bind_retained_with(&mut fixture.kernel(), &root, &source, &mapping) + .map_err(|failure| failure.error) + }; // Onto targets that a container root's mapper refuses, creating what they lack. bind(mapping("file.txt", "/etc/machine-id", Access::ReadOnly)).unwrap(); bind(mapping("dir", "/run/nvx/time", Access::ReadOnly)).unwrap(); @@ -953,7 +967,7 @@ fn runtime_binds_unbind_in_reverse_order_and_report_each_failure() { ] .map(|(source_path, target)| { let mapping = mapping(source_path, target, Access::ReadOnly); - bind_with(&mut fixture.kernel(), &root, &source, &mapping).unwrap() + bind_retained_with(&mut fixture.kernel(), &root, &source, &mapping).unwrap() }) }; let points = [ @@ -1239,7 +1253,7 @@ fn an_empty_source_maps_the_source_root_itself_from_beneath_it() { ) ); assert!(fixture.path("root/whole").is_dir()); - mapper.teardown_with(&mut fixture.kernel()).unwrap(); + assert_eq!(mapper.unmount_with(&mut fixture.kernel()), Vec::new()); // It is a directory, which goes only onto a directory, and only where a mapping of a // directory may go. let calls = fixture.kernel.calls(); @@ -1332,3 +1346,19 @@ fn errors_name_what_failed() { ) ); } + +/// Compiles a mapper's steps, as the module's example showed them, without mounting anything: a +/// mapper beneath a guest's new root maps the host export's `0/workspace` at `/workspace`, +/// read-write, `nosuid`, and `nodev`, then unmounts it. +#[test] +fn a_mapper_s_steps_compile() { + let _steps = || -> Result<(), Box> { + let root = TargetRoot::open(Path::new("/newroot"))?; + let mut mapper = Mapper::new(root, Reserved::container_root()); + let export = SourceRoot::open(Path::new("/run/export"))?; + let workspace = Mapping::new("0/workspace", "/workspace", Access::ReadWrite)?; + mapper.apply_with(&mut SystemKernel, &export, &[workspace])?; + assert!(mapper.unmount_with(&mut SystemKernel).is_empty()); + Ok(()) + }; +} diff --git a/nvx-rs/src/guest/mappings/mapper/tests/privileged.rs b/nvx-rs/src/guest/mappings/mapper/tests/privileged.rs index a03f3c019..2ce0c2fc0 100644 --- a/nvx-rs/src/guest/mappings/mapper/tests/privileged.rs +++ b/nvx-rs/src/guest/mappings/mapper/tests/privileged.rs @@ -95,8 +95,10 @@ impl Scene { fn refusal(&self, reserved: Reserved, source: &str, target: &str) -> MappingError { let mut mapper = self.mapper(reserved); let mapping = Mapping::new(source, target, Access::ReadWrite).unwrap(); - let error = mapper.apply(&self.source(), &[mapping]).unwrap_err(); - assert!(mapper.is_latched()); + let error = mapper + .apply_with(&mut SystemKernel, &self.source(), &[mapping]) + .unwrap_err(); + assert!(mapper.latched); error.error().clone() } } @@ -297,7 +299,7 @@ fn a_rename_race_never_mounts_outside_the_target() { for attempt in 0..2000 { let mut mapper = scene.mapper(Reserved::default()); let mapping = Mapping::new("dir", format!("/a/x{attempt}"), Access::ReadOnly).unwrap(); - match mapper.apply(&source, &[mapping]) { + match mapper.apply_with(&mut SystemKernel, &source, &[mapping]) { Ok(()) => attached += 1, Err(error) => match error.error() { MappingError::Link { .. } => linked += 1, @@ -342,13 +344,17 @@ fn kinds_nested_targets_and_reserved_targets_are_checked() { Mapping::new("dir", "/m", Access::ReadWrite).unwrap(), Mapping::new("file.txt", "/m/marker", Access::ReadOnly).unwrap(), ]; - mapper.apply(&scene.source(), &mappings).unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &mappings) + .unwrap(); let (outer, outer_root) = mount_of(&root.join("m")); let (inner, inner_root) = mount_of(&root.join("m/marker")); assert!(outer_root && inner_root && outer != inner); assert_eq!(fs::read(root.join("m/marker")).unwrap(), b"file"); let nested = Mapping::new("dir/inner", "/m/new", Access::ReadOnly).unwrap(); - let error = mapper.apply(&scene.source(), &[nested]).unwrap_err(); + let error = mapper + .apply_with(&mut SystemKernel, &scene.source(), &[nested]) + .unwrap_err(); assert_eq!( error.error(), &MappingError::Foreign { @@ -378,7 +384,7 @@ fn kinds_nested_targets_and_reserved_targets_are_checked() { ); } assert!(!root.join("run").exists() && !root.join("etc").exists()); - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); assert!(mounts_below(&root).is_empty()); } @@ -403,7 +409,9 @@ fn an_empty_source_maps_a_copy_of_the_source_root_s_own_mount() { std::os::unix::fs::symlink("dir", export.join("link")).unwrap(); let mut mapper = scene.mapper(Reserved::default()); let whole = Mapping::new("", "/whole", Access::ReadOnly).unwrap(); - mapper.apply(&scene.source(), &[whole]).unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &[whole]) + .unwrap(); // The target is the root of a copy of the source root's own mount, read-only, `nosuid`, and // `nodev`, which shows the whole export, but not the file system mounted below it. let mapped = root.join("whole"); @@ -431,7 +439,7 @@ fn an_empty_source_maps_a_copy_of_the_source_root_s_own_mount() { scene.refusal(Reserved::default(), "dir/inner", "/inner"), refused(Errno::EXDEV) ); - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); assert!(mounts_below(&root).is_empty()); assert!(!export.join("new").exists()); } @@ -464,13 +472,15 @@ fn a_mapping_covers_the_root_of_a_mount_only_if_the_root_owns_it() { target.add_mount(&tmp).unwrap(); let mut mapper = Mapper::new(target, Reserved::default()); let mapping = Mapping::new("dir", "/tmp", Access::ReadWrite).unwrap(); - mapper.apply(&scene.source(), &[mapping]).unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &[mapping]) + .unwrap(); let (covering, covering_root) = mount_of(&tmp); assert!(covering_root && covering != tmpfs); assert_eq!(fs::read(tmp.join("marker")).unwrap(), b"host"); assert!(!tmp.join("own").exists()); // The teardown uncovers it. - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); assert_eq!(mount_of(&tmp), (tmpfs, true)); assert_eq!(fs::read(tmp.join("own")).unwrap(), b"tmpfs"); assert_eq!(mounts_below(&root), [tmp]); @@ -505,7 +515,9 @@ fn a_workload_that_is_not_root_enters_what_a_mapper_created_whatever_the_umask_o mappings.push(Mapping::new("dir", share, Access::ReadOnly).unwrap()); mappings.push(Mapping::new("file.txt", file, Access::ReadOnly).unwrap()); } - mapper.apply(&scene.source(), &mappings).unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &mappings) + .unwrap(); let mode = |path: &Path| fs::metadata(path).unwrap().permissions().mode() & 0o7777; for directory in ["srv"].iter().chain(parents) { assert_eq!(mode(&root.join(directory)), 0o755, "{directory}"); @@ -533,7 +545,7 @@ fn a_workload_that_is_not_root_enters_what_a_mapper_created_whatever_the_umask_o for ((path, contents), read) in expected.iter().zip(read) { assert_eq!(read.unwrap(), *contents, "{}", path.display()); } - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); }) .join() .unwrap(); @@ -571,11 +583,13 @@ fn a_read_only_mapping_is_never_visible_writable() { for _ in 0..300 { let mut mapper = scene.mapper(Reserved::default()); let mapping = Mapping::new("dir", "/ro", Access::ReadOnly).unwrap(); - mapper.apply(&source, &[mapping]).unwrap(); + mapper + .apply_with(&mut SystemKernel, &source, &[mapping]) + .unwrap(); let flags = statvfs(&scene.path("root/ro")).unwrap().flags(); assert!(flags.contains(FsFlags::ST_RDONLY | FsFlags::ST_NOSUID | FsFlags::ST_NODEV)); let _teardown = attempts.write().unwrap(); - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); } stop.store(true, Ordering::Relaxed); racer.join().unwrap(); @@ -584,13 +598,15 @@ fn a_read_only_mapping_is_never_visible_writable() { // A writable mapping is writable, and still `nosuid` and `nodev`. let mut mapper = scene.mapper(Reserved::default()); let mapping = Mapping::new("dir", "/rw", Access::ReadWrite).unwrap(); - mapper.apply(&source, &[mapping]).unwrap(); + mapper + .apply_with(&mut SystemKernel, &source, &[mapping]) + .unwrap(); let flags = statvfs(&scene.path("root/rw")).unwrap().flags(); assert!(flags.contains(FsFlags::ST_NOSUID | FsFlags::ST_NODEV)); assert!(!flags.contains(FsFlags::ST_RDONLY)); fs::write(scene.path("root/rw/marker"), b"guest").unwrap(); assert_eq!(fs::read(scene.path("export/dir/marker")).unwrap(), b"guest"); - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); } #[test] @@ -606,21 +622,22 @@ fn a_teardown_unmounts_each_mapping_and_reports_a_busy_one() { Mapping::new("dir/inner", "/busy/inner", Access::ReadOnly).unwrap(), Mapping::new("file.txt", "/deep/file", Access::ReadOnly).unwrap(), ]; - mapper.apply(&scene.source(), &mappings).unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &mappings) + .unwrap(); assert_eq!(mounts_below(&root).len(), 3); // An open file keeps its mapping busy, which the teardown reports and leaves. let held = fs::File::open(root.join("busy/marker")).unwrap(); - let error = mapper.teardown().unwrap_err(); + let failures = mapper.unmount_with(&mut SystemKernel); assert_eq!( - error - .failures() + failures .iter() - .map(UnmountFailure::target) + .map(|(_, failure)| failure.target()) .collect::>(), [Path::new("/busy")] ); assert_eq!( - error.failures()[0].error(), + failures[0].1.error(), &MappingError::System { step: Step::Unmount, error: Errno::EBUSY as i32 @@ -631,8 +648,10 @@ fn a_teardown_unmounts_each_mapping_and_reports_a_busy_one() { nix::mount::umount(&root.join("busy")).unwrap(); // Without one, every mapping goes. let mut mapper = scene.mapper(Reserved::default()); - mapper.apply(&scene.source(), &mappings).unwrap(); - mapper.teardown().unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &mappings) + .unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); assert!(mounts_below(&root).is_empty()); assert!(root.join("deep/file").is_file()); } @@ -655,9 +674,11 @@ fn a_thread_with_a_descriptor_table_of_its_own_tears_its_mappings_down() { Mapping::new("dir", "/a/b", Access::ReadOnly).unwrap(), Mapping::new("file.txt", "/c", Access::ReadOnly).unwrap(), ]; - mapper.apply(&scene.source(), &mappings).unwrap(); + mapper + .apply_with(&mut SystemKernel, &scene.source(), &mappings) + .unwrap(); assert_eq!(mounts_below(&root).len(), 2); - mapper.teardown().unwrap(); + assert_eq!(mapper.unmount_with(&mut SystemKernel), Vec::new()); assert!(mounts_below(&root).is_empty()); }) .join() diff --git a/nvx-rs/src/guest/mappings/table.rs b/nvx-rs/src/guest/mappings/table.rs index 0bb234d81..fc289bf96 100644 --- a/nvx-rs/src/guest/mappings/table.rs +++ b/nvx-rs/src/guest/mappings/table.rs @@ -21,8 +21,8 @@ use crate::protocol::{self, GuestHostMap, MAX_HOST_MAPPINGS, ProtocolError}; /// parts, each a `MAPS` payload of consecutive entries, in order, which /// [`protocol::decode_guest_maps_payload`] reads. [`apply`](Self::apply) checks each part against /// the table, and hands its entries, as [`Mapping`]s, to the caller, which maps them beneath the -/// root of its choice, from the host export: through a mapper beneath a guest's own `/` or a -/// container's root, for example, or through a layered root. +/// root of its choice, from the host export: through a mapper beneath a guest's own `/`, a +/// container's root, or a layered root, for example. /// /// - The table refuses a part as a whole, and changes nothing, which [`TableError::Refused`] /// reports with the entry that the table expects next and the [`Refusal`]: once a mapping @@ -37,8 +37,8 @@ use crate::protocol::{self, GuestHostMap, MAX_HOST_MAPPINGS, ProtocolError}; /// announced, and none failed: only then may the agent start workloads. A table of zero /// mappings is complete at once, and its agent maps nothing. /// -/// The root keeps the mappings, and its own teardown tears the table down, in reverse order, such -/// as a mapper's teardown, which reports each mapping that stays mounted, or a layered root's. +/// The table unmounts nothing: the caller keeps the mappings that it applied, and unmounts them +/// itself, in reverse order, if it ever does. /// /// NVX's managed agent applies its table through one, beneath its own root, `/`, in direct mode, /// with a mapper that it opens at the first part that the table hands it, so that a table of zero diff --git a/nvx-rs/src/guest/mod.rs b/nvx-rs/src/guest/mod.rs index b3cf61b8a..d8513e13f 100644 --- a/nvx-rs/src/guest/mod.rs +++ b/nvx-rs/src/guest/mod.rs @@ -18,7 +18,7 @@ //! The guest's space, the root that an init assembles and what it mounts in it, is each init's //! own. NVX's own space is internal reference code, which NVX's guest programs use: it is hidden //! from this documentation and unstable, and it becomes crate-private once nothing outside NVX -//! uses it. It is `root`, which assembles a layered root, mounts live shares and mappings in it, +//! uses it. It is `root`, which assembles a layered root, mounts live shares in it, //! and tears it down; `view`, which sets the VM's hostname, //! machine ID, and resolver file, whose binds the root owns; `gpt`, which reads the partition //! table of a GPT disk that the kernel attached as a block device; the mapper of `mappings`, which diff --git a/nvx-rs/src/guest/root.rs b/nvx-rs/src/guest/root.rs index b6017272e..2b2d928bf 100644 --- a/nvx-rs/src/guest/root.rs +++ b/nvx-rs/src/guest/root.rs @@ -1,6 +1,6 @@ -//! Operations that assemble a layered root, mount live shares and mappings in it, and the host -//! export from which mappings come, and tear it down, for a guest init or agent, whether or not -//! it is PID 1. +//! Operations that assemble a layered root, mount live shares in it, and the host export from +//! which a caller's mappings come, and tear it down, for a guest init or agent, whether or not it +//! is PID 1. //! //! This module is NVX's internal reference code of the guest's space, hidden from the //! documentation and unstable: it becomes crate-private once nothing outside NVX uses it. @@ -10,24 +10,22 @@ //! the caller's [`OverlayMode`], which it checks the kernel applied, of the read-only layers of //! the `distro` image over an upper. The caller names the image's format, which the assembly //! never probes: an EROFS file system, the overlay's one lower layer, or a GPT disk, whose first -//! partition holds opaque bytes that the root returns, and whose later partitions hold the -//! layers, ext4 file systems. The upper is an ext4 scratch or a tmpfs. It returns the [`Root`] -//! that owns the mounts, holds the [runtime directory](#runtime-directory), and reports what it -//! mounted: the first partition's bytes, the [`Lower`] layers, the [`UpperKind`], and the -//! [`OverlayMode`]. +//! partition holds opaque bytes that the assembly reads within the caller's bound, and whose +//! later partitions hold the layers, ext4 file systems. The upper is an ext4 scratch or a tmpfs. +//! It returns the [`Root`] that owns the mounts and holds the +//! [runtime directory](#runtime-directory). //! - [`Root::mount_share`] and [`Root::mount_aggregate`] mount the VMM's live shares at targets in -//! the root, [`Root::mount_export`] mounts the host export out of the root, and [`Root::map`] -//! applies a caller's host mappings in the root, from the export or another source, through the -//! root's [`Mapper`], as [live shares and mappings](#live-shares-and-mappings) describes. +//! the root, through the root's [`Mapper`], and [`Root::mount_export`] mounts the host export +//! out of the root, as [live shares and mappings](#live-shares-and-mappings) describes. //! - `Root::mount_runtime`, which is crate-private, mounts the runtime's file systems in the root, -//! as an init does once for its VM, before the root's first live share or mapping: it makes +//! as an init does once for its VM, before the root's first live share: it makes //! every mount of the root's tree `nosuid` and `nodev`, then mounts `/proc`, `/sys`, `/dev` with //! `/dev/pts` and `/dev/shm`, and `/run`, and binds the guest's time status at `/run/nvx/time`. //! - `Root::switch`, which is crate-private, switches into the root, as an init does once for its //! VM: it moves the root's mount onto `/`, then makes the root the caller's root directory and //! working directory. The root's mounts stay from then on: its teardown refuses, and dropping it //! unmounts nothing. -//! - [`Root::teardown`] unmounts, in order, the live shares and mappings in reverse order, the +//! - [`Root::teardown`] unmounts, in order, the live shares in reverse order, the //! mounts of an aggregate live share, of the host export, and of the runtime, the later first, //! the overlay, the layers from the top down, and the upper, whose file system it syncs first //! if the upper is an ext4 scratch. It attempts each, whatever failed before, and reports every @@ -104,62 +102,22 @@ //! each mount with its [`Access`], `nosuid`, and `nodev` set already. //! The root mounts a share's device at `staging`, with them, then moves it onto its target, and //! mounts an aggregate's device at `shares`, then attaches a copy of each child's entry there. -//! [`Root::map`] applies a caller's host [`Mapping`]s beneath the -//! overlay the same way, from the caller's [`SourceRoot`]. //! //! The host export is the device of an aggregate whose children the root never binds itself: //! they are the sources of the caller's mappings, such as the mapping table of NVX's managed //! lifecycle, whose boot tokens [`protocol::export`](crate::protocol::export) reads. //! [`Root::mount_export`] mounts it at `hostfs/root`, out of the root, with the access of its //! [`ExportMode`](crate::protocol::export::ExportMode), `nosuid`, and `nodev`, and returns its -//! root, the [`SourceRoot`] from which [`Root::map`] applies the -//! caller's mappings. A root has one export at most. +//! root, the [`SourceRoot`] from which the caller's own [`Mapper`] applies its mappings beneath +//! the root. A root has one export at most. //! //! The mounts are made in the caller's mount namespace, which must be recursively private, as a //! guest's initial namespace is. Mount-point checks and mount-option reads use the calling //! thread's descriptor table and mount namespace, even when it shares neither with the process's -//! first thread. A share, a child, the export, or a mapping that fails once the root has checked -//! its request latches the root: the root refuses every later share, export, and -//! mapping, so that its caller starts no workload on a partial set, and the mounts before it stay -//! until the teardown, which unmounts them first. -//! -//! ```no_run -//! use std::path::Path; -//! use std::time::Duration; -//! -//! use nvx::gpt::Expected; -//! use nvx::guest::mappings::{Access, Mapping}; -//! use nvx::guest::root::{self, Assembly, Device, Image, OverlayMode, Upper}; -//! use nvx::protocol::export::HostExport; -//! use nvx::protocol::root::DISTRO_MMIO_BASE; -//! -//! # fn main() -> Result<(), Box> { -//! let mut root = root::assemble(&Assembly { -//! runtime: "/run/nvx".into(), -//! distro: Device { mmio: DISTRO_MMIO_BASE, sectors: None }, -//! image: Image::Gpt { -//! expected: Expected::new().disk_guid("8d2f3a4e-1c5b-4e7a-9f60-2b3c4d5e6f70".parse()?), -//! first_partition_bound: 64 * 1024, -//! }, -//! upper: Upper::Tmpfs { max_memory_percent: Some(50) }, -//! overlay: OverlayMode::UntrustedLowers, -//! timeout: Duration::from_secs(30), -//! })?; -//! let configuration = root.first_partition().unwrap_or_default(); -//! let layers = root.lowers().len(); -//! println!("the first partition holds {} bytes over {layers} layers", configuration.len()); -//! root.mount_share("workspace", Path::new("/workspace"), Access::ReadWrite)?; -//! // The host export that the kernel command line names, if any, is the source of host mappings. -//! if let Some(export) = HostExport::parse(&std::fs::read("/proc/cmdline")?)? { -//! let source = root.mount_export(&export.tag, export.mode.into())?; -//! root.map(&source, &[Mapping::new("0/src", "/src", Access::ReadOnly)?])?; -//! // Dropping the export's root here lets the teardown unmount the export. -//! } -//! // Run the workload in the root at `root.path()`; then: -//! root.teardown()?; -//! # Ok(()) -//! # } -//! ``` +//! first thread. A share, a child, or the export that fails once the root has checked its request +//! latches the root: the root refuses every later share and export, so that its caller starts no +//! workload on a partial set, and the mounts before it stay until the teardown, which unmounts +//! them first. mod block; mod kernel; @@ -372,8 +330,8 @@ pub enum Image { /// [`guest::gpt::read_disk`](super::gpt::read_disk), and whose partitions it uses as the /// kernel created them when it found the disk, or when it last reread the table, as a clone /// asks of it before its assembly: the assembly neither compares them with the table nor asks - /// the kernel to reread it. Its first partition holds opaque bytes, which the assembly - /// returns through [`Root::first_partition`], and never probes or mounts. Each later + /// the kernel to reread it. Its first partition holds opaque bytes, which the assembly reads, + /// and never probes or mounts. Each later /// partition, from partition 2 on, holds a layer, an ext4 file system that the assembly /// mounts at `layers/pN`, for the partition's number N, `ro,nosuid,nodev`, with /// `noload,errors=remount-ro`: it never loads or replays the journal, whose scan can hang @@ -426,7 +384,7 @@ impl Upper { } } -/// The kind of a root's upper, which [`Root::upper`] reports. +/// The kind of a root's upper. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum UpperKind { @@ -669,33 +627,8 @@ pub struct Lower { partition: Option, } -impl Lower { - /// Returns the layer's mount: [`Mount::Distro`] for an EROFS image, or [`Mount::Layer`] for a - /// layer of a GPT disk. - pub fn mount(&self) -> &Mount { - &self.mount - } - - /// Returns the node of the layer's block device: the distro device that holds an EROFS image, - /// or the block device of a GPT disk's partition. - pub fn device(&self) -> &Path { - &self.device - } - - /// Returns the layer's mount point, below the runtime directory. - pub fn path(&self) -> &Path { - &self.path - } - - /// Returns the GPT partition that holds the layer, with its number, GUIDs, and extent, or - /// `None` for an EROFS image. - pub fn partition(&self) -> Option<&Partition> { - self.partition.as_ref() - } -} - /// The handle of an assembled layered root, which owns its mounts, those of the assembly and -/// those of its live shares, host export, and mappings, and reports what the assembly mounted. +/// those of its live shares and host export. /// /// [`teardown`](Self::teardown) unmounts them and reports each failure. Dropping the root /// unmounts them the same way, and ignores failures, unless `Root::switch` has switched into the @@ -731,14 +664,14 @@ pub struct Root { view_binds: Option<(TargetRoot, Vec)>, /// A view bind whose attachment failed and which could not be detached. view_stray: Option<(PathBuf, FileDescriptor)>, - /// The mapper of the live shares and mappings in the root, once the root has mounted one, whose - /// own mounts are the overlay and the runtime's `/run` tmpfs, if the root held it then. + /// The mapper of the live shares in the root, once the root has mounted one, whose own mounts + /// are the overlay and the runtime's `/run` tmpfs, if the root held it then. mapper: Option, /// What each of the mapper's mappings mounts, in the mapper's order, then what the one that /// a rename moved and that the mapper failed to detach mounts, if any. mapped: Vec, - /// Whether a share, the export, a mapping, or a view bind failed, after which the root refuses - /// later shares, exports, mappings, and view applications. + /// Whether a share, the export, or a view bind failed, after which the root refuses later + /// shares, exports, and view applications. latched: bool, /// Whether the root's mount has moved onto `/`, after which the root refuses another switch /// and its teardown, and unmounts nothing. @@ -810,40 +743,9 @@ impl Root { Ok(()) } - /// Returns whether a live share, the host export, a mapping, or a view bind failed, after which - /// the root refuses later shares, exports, mappings, and views. - pub fn is_latched(&self) -> bool { - self.latched - } - - /// Returns the bytes of a GPT disk's first partition: all of its bytes, less its trailing NUL - /// bytes and ASCII whitespace, as opaque bytes that need not be UTF-8 and that the root never - /// interprets. Returns `None` for an EROFS image. - pub fn first_partition(&self) -> Option<&[u8]> { - self.first_partition.as_deref() - } - - /// Returns the overlay's layers, the base first: an EROFS image, or the layers of a GPT disk - /// in the order of their partitions, from partition 2's, the base, to the last partition's, - /// the top. - pub fn lowers(&self) -> &[Lower] { - &self.lowers - } - - /// Returns the kind of the root's upper. - pub fn upper(&self) -> UpperKind { - self.upper_kind - } - - /// Returns the overlay's mode, whose option set, [`OverlayMode::options`], the assembly found - /// in effect. - pub fn overlay(&self) -> OverlayMode { - self.overlay_mode - } - - /// Unmounts the root's mounts, in order: the view's binds, a live share or a mapping that a rename moved and - /// that its mapper failed to detach, which the mapper detaches, the live shares and mappings - /// in reverse order, a runtime mount whose attachment failed and which could not be detached, + /// Unmounts the root's mounts, in order: the view's binds, a live share that a rename moved and + /// that its mapper failed to detach, which the mapper detaches, the live shares in reverse + /// order, a runtime mount whose attachment failed and which could not be detached, /// the mounts of an aggregate live share, of the host export, and of the runtime, the later /// first, the overlay, the layers from the top down, and the upper. /// @@ -888,10 +790,10 @@ impl Root { /// is mounted already is refused as [`Error::Occupied`]. So is a call while the root holds a /// runtime mount, before it changes anything, since restricting the tree again would make an /// earlier call's `/dev` and `/dev/pts` `nodev`: a second call changes nothing. So is a call - /// while the root holds a live share or a mapping, before it changes anything too, since the - /// `/run` tmpfs would hide one beneath `/run` from the teardown: the runtime comes first. A - /// later share or mapping beneath `/run` is on the `/run` tmpfs, where the root's mapper - /// creates what its target lacks, and the teardown unmounts it before the tmpfs. + /// while the root holds a live share, before it changes anything too, since the `/run` tmpfs + /// would hide one beneath `/run` from the teardown: the runtime comes first. A later share + /// beneath `/run` is on the `/run` tmpfs, where the root's mapper creates what its target + /// lacks, and the teardown unmounts it before the tmpfs. /// The mounts belong to the root, which unmounts them, the later first, when it is torn down. /// If one fails, the call unmounts those that it made, the later first, and returns the /// failure; one that fails to unmount stays for the teardown. The tree stays `nosuid` and @@ -935,10 +837,8 @@ impl Root { { return Err(Error::Occupied { path: path.clone() }); } - // The `/run` tmpfs would hide a share or a mapping beneath `/run` from the teardown. - if let Some( - Mount::Share { target, .. } | Mount::Child { target, .. } | Mount::Mapping { target }, - ) = self.mapped.first() + // The `/run` tmpfs would hide a share beneath `/run` from the teardown. + if let Some(Mount::Share { target, .. } | Mount::Child { target, .. }) = self.mapped.first() { let path = self.path.join(target.strip_prefix("/").unwrap_or(target)); return Err(Error::Occupied { path }); @@ -1029,8 +929,7 @@ impl Root { /// /// Paths resolve in the root from then on, but descriptors opened before the switch stay /// usable: a [`Mapper`] still maps from a [`SourceRoot`](super::mappings::SourceRoot) beneath - /// a [`TargetRoot`] opened before it, as [`map`](Self::map) does if the root had mounted a - /// live share or a mapping before it. + /// a [`TargetRoot`] opened before it. pub(crate) fn switch(&mut self) -> Result<(), SwitchError> { if self.switched { return Err(SwitchError::Switched); @@ -1415,7 +1314,6 @@ impl Root { mount: Mount::Mapping { target: target.clone(), }, - path: self.path.join(target.strip_prefix("/").unwrap_or(&target)), step: Step::Unmount, error: error.into(), }); @@ -1429,7 +1327,6 @@ impl Root { mount: Mount::Mapping { target: target.to_owned(), }, - path: self.path.join(target.strip_prefix("/").unwrap_or(target)), step: Step::Unmount, error: share::io_error(failure.error()), }); @@ -1448,7 +1345,6 @@ impl Root { }); failures.push(Unmount { mount, - path: self.path.join(target.strip_prefix("/").unwrap_or(target)), step: Step::Unmount, error: share::io_error(failure.error()), }); @@ -1459,7 +1355,6 @@ impl Root { { failures.push(Unmount { mount: Mount::Runtime(runtime), - path: self.path.join(&runtime.target()[1..]), step: Step::Unmount, error: error.into(), }); @@ -1489,7 +1384,6 @@ impl Root { if let Err(error) = outcome { failures.push(Unmount { mount, - path, step, error: error.into(), }); @@ -1558,9 +1452,9 @@ pub(crate) fn bind_file_retained( } /// Binds the file or directory at `path`, which must be of `kind`, read-only at `target` beneath -/// `root`, with `kernel`, through the walk and attach of [`mappings::bind`] from a source root at -/// `path`'s parent directory, and returns the bind, retaining a failed bind that its caller must -/// detach. +/// `root`, with `kernel`, through the walk and attach of a runtime bind, +/// [`bind_retained_with`](mappings::mapper::bind_retained_with), from a source root at `path`'s +/// parent directory, and returns the bind, retaining a failed bind that its caller must detach. pub(crate) fn bind_runtime_file_retained( kernel: &mut dyn mappings::Kernel, root: &TargetRoot, @@ -1943,7 +1837,7 @@ pub enum Mount { /// The child's target in the root. target: PathBuf, }, - /// A caller's host mapping, at its target in the root. + /// A bind at its target in the root, such as one of the view's. Mapping { /// The mapping's target in the root. target: PathBuf, @@ -2202,10 +2096,10 @@ pub enum Error { /// The error. error: io::Error, }, - /// A live share, a child of an aggregate live share, or a mapping was refused or failed to - /// attach at its target in the root, once the root had checked its request; nothing of it - /// stays mounted, unless the error is a moved mapping that stays mounted. The root refuses - /// every later share, export, and mapping, with [`MappingError::Latched`]. + /// A live share or a child of an aggregate live share was refused or failed to attach at its + /// target in the root, once the root had checked its request; nothing of it stays mounted, + /// unless the error is a moved mapping that stays mounted. The root refuses every later share + /// and export, with [`MappingError::Latched`]. Mapping { /// The mount that was refused or failed. mount: Mount, @@ -2376,23 +2270,6 @@ pub struct AssemblyError { cleanup: Option, } -impl AssemblyError { - /// Returns the assembly's failure. - pub fn error(&self) -> &Error { - &self.error - } - - /// Returns the failures to unmount what the assembly had mounted, if any: those mounts stay. - pub fn cleanup(&self) -> Option<&TeardownError> { - self.cleanup.as_ref() - } - - /// Returns the assembly's failure and the cleanup's. - pub fn into_parts(self) -> (Error, Option) { - (self.error, self.cleanup) - } -} - impl fmt::Display for AssemblyError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { self.error.fmt(formatter)?; @@ -2449,12 +2326,6 @@ pub struct TeardownError { } impl TeardownError { - /// Returns the failures, each of a mount that stays in place, in the order in which they were - /// attempted: none if the root had switched. - pub fn failures(&self) -> &[Unmount] { - &self.failures - } - /// Returns whether the teardown was refused because the root had /// [switched](Root::switch), so that it unmounted nothing. pub(crate) fn is_switched(&self) -> bool { @@ -2486,8 +2357,6 @@ impl std::error::Error for TeardownError {} pub struct Unmount { /// The mount. mount: Mount, - /// Its mount point. - path: PathBuf, /// The step that failed. step: Step, /// The error. @@ -2503,23 +2372,6 @@ enum Step { Unmount, } -impl Unmount { - /// Returns the mount that stays in place. - pub fn mount(&self) -> &Mount { - &self.mount - } - - /// Returns its mount point. - pub fn path(&self) -> &Path { - &self.path - } - - /// Returns the error of its sync or of its unmount. - pub fn error(&self) -> &io::Error { - &self.error - } -} - impl fmt::Display for Unmount { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let step = match self.step { diff --git a/nvx-rs/src/guest/root/share.rs b/nvx-rs/src/guest/root/share.rs index 321c54a42..526ce122b 100644 --- a/nvx-rs/src/guest/root/share.rs +++ b/nvx-rs/src/guest/root/share.rs @@ -1,5 +1,5 @@ -//! Live shares, virtio-fs devices that the VMM exports from the host, and a caller's mappings, -//! mounted at targets in a layered root through the root's mapper, after the checks that the +//! Live shares, virtio-fs devices that the VMM exports from the host, mounted at targets in a +//! layered root through the root's mapper, after the checks that the //! [module](super#live-shares-and-mappings) lists, and the host export, the device from which a //! caller's mappings come. @@ -250,9 +250,8 @@ impl Root { } /// Mounts the host export, the aggregate virtio-fs device `tag` from which the caller maps host - /// paths into the root with [`map`](Self::map), and returns the export's root, the - /// [`SourceRoot`] of those mappings. A guest reads the export's tag and [`ExportMode`] from - /// its kernel command line with + /// paths into the root, and returns the export's root, the [`SourceRoot`] of those mappings. A + /// guest reads the export's tag and [`ExportMode`] from its kernel command line with /// [`HostExport::parse`](crate::protocol::export::HostExport::parse), and mounts it with the /// mode's access. /// @@ -266,9 +265,9 @@ impl Root { /// live share's is; nothing is created or mounted for a refused one. /// /// The mount belongs to the root, which unmounts it when it is torn down, after the live - /// shares and mappings, those mapped from the export included. The returned [`SourceRoot`] - /// holds a descriptor of the export's root, which keeps the mount busy: drop it before the - /// teardown, which otherwise fails to unmount the export, and leaves it mounted. + /// shares. The returned [`SourceRoot`] holds a descriptor of the export's root, which keeps the + /// mount busy: drop it before the teardown, which otherwise fails to unmount the export, and + /// leaves it mounted. /// /// A root has one export at most, since every export is mounted at the same place: once the /// root has mounted one, another is refused. A failure once the tag passed its check latches @@ -291,24 +290,6 @@ impl Root { mounted } - /// Applies `mappings` beneath the root, in order, from `source`, such as the guest's mount of - /// the host's export, through the root's mapper, as the - /// [module](super#live-shares-and-mappings) describes, and owns them: the teardown unmounts - /// them first, with the live shares, in reverse order. - /// - /// Zero mappings do no work. The first mapping that is refused or fails is reported, with - /// the target that names it, as [`Error::Mapping`]; the mappings before it stay, and the root - /// is latched. - pub fn map(&mut self, source: &SourceRoot, mappings: &[Mapping]) -> Result<(), Error> { - let Some(first) = mappings.first() else { - return Ok(()); - }; - self.check_latch(&mapping_mount(first))?; - let mapped = self.apply(source, mappings); - self.latched |= mapped.is_err(); - mapped - } - /// Mounts the device `tag` at `staging` and moves it onto the relative `target`, as /// [`mount_share`](Self::mount_share) describes. fn stage_share( @@ -452,15 +433,6 @@ impl Root { }) } - /// Applies `mappings` from `source` through the root's mapper, as [`map`](Self::map) - /// describes. - fn apply(&mut self, source: &SourceRoot, mappings: &[Mapping]) -> Result<(), Error> { - for mapping in mappings { - self.apply_one(source, &mapping_mount(mapping), mapping)?; - } - Ok(()) - } - /// Applies `mapping` from `source` through the root's mapper, and owns it as `mount`. fn apply_one( &mut self, @@ -570,13 +542,6 @@ impl Root { } } -/// Returns the mount of the caller's `mapping`, which its absolute target names. -fn mapping_mount(mapping: &Mapping) -> Mount { - Mount::Mapping { - target: Path::new("/").join(mapping.target()), - } -} - /// Opens the mount point `path` with `O_PATH`, not through a final symbolic link. pub(super) fn open_mount_point(path: &Path) -> Result { let flags = diff --git a/nvx-rs/src/guest/root/tests.rs b/nvx-rs/src/guest/root/tests.rs index ed7f87021..b9c9c58af 100644 --- a/nvx-rs/src/guest/root/tests.rs +++ b/nvx-rs/src/guest/root/tests.rs @@ -41,6 +41,12 @@ const LAYER_BLOCKS: usize = 4; /// The tests' bound for the first partition, in bytes. const FIRST_PARTITION_BOUND: usize = 64 * 1024; +/// Returns why the assembly of `error` failed, for the tests of a clone's application, whose +/// module can't read the field. +pub(crate) fn assembly_failure(error: &AssemblyError) -> &Error { + &error.error +} + /// A guest in a temporary directory, and the fake kernel that the operations get clones of. struct Guest { /// The temporary directory, which the guest holds only so that dropping it removes the @@ -510,16 +516,16 @@ fn assembly_mounts_the_image_and_the_scratch_under_the_overlay() { assert_eq!(root.path(), guest.path("run/nvx/rootfs")); assert_eq!(root.runtime(), guest.path("run/nvx")); // The root reports what it mounted: the image's one layer, the scratch, and the mode. - assert_eq!(root.first_partition(), None); - let [lower] = root.lowers() else { - panic!("{:?}", root.lowers()); + assert_eq!(root.first_partition.as_deref(), None); + let [lower] = root.lowers.as_slice() else { + panic!("{:?}", root.lowers); }; - assert_eq!(lower.mount(), &Mount::Distro); - assert_eq!(lower.device(), guest.path("dev/vda")); - assert_eq!(lower.path(), guest.path("run/nvx/layers/distro")); - assert_eq!(lower.partition(), None); - assert_eq!(root.upper(), UpperKind::Scratch); - assert_eq!(root.overlay(), OverlayMode::TrustedLowers); + assert_eq!(lower.mount, Mount::Distro); + assert_eq!(lower.device, guest.path("dev/vda")); + assert_eq!(lower.path, guest.path("run/nvx/layers/distro")); + assert_eq!(lower.partition, None); + assert_eq!(root.upper_kind, UpperKind::Scratch); + assert_eq!(root.overlay_mode, OverlayMode::TrustedLowers); root.teardown().unwrap(); } @@ -545,7 +551,7 @@ fn devices_are_found_when_they_appear_and_missing_ones_time_out() { let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Missing { role: Role::Scratch, mmio: SCRATCH, @@ -553,7 +559,7 @@ fn devices_are_found_when_they_appear_and_missing_ones_time_out() { ), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!(guest.kernel.sleeps(), 2); assert_eq!( error.to_string(), @@ -575,7 +581,7 @@ fn an_assembly_waits_for_its_devices_until_its_deadline_at_the_latest() { let error = guest.assemble_until(&assembly, Instant::now()).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Missing { role: Role::Scratch, mmio: SCRATCH, @@ -594,7 +600,7 @@ fn an_assembly_waits_for_its_devices_until_its_deadline_at_the_latest() { let error = guest.assemble_until(&assembly, deadline).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Missing { role: Role::Scratch, .. @@ -616,7 +622,7 @@ fn an_assembly_waits_for_its_devices_until_its_deadline_at_the_latest() { let error = guest.assemble_until(&assembly, deadline).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Missing { role: Role::Scratch, .. @@ -669,7 +675,7 @@ fn a_device_of_another_size_is_refused_before_it_is_flushed() { let guest = Guest::new(); let mut assembly = guest.assembly(); assembly.distro.sectors = Some(2048); - let error = guest.assemble(&assembly).unwrap_err().into_parts().0; + let error = guest.assemble(&assembly).unwrap_err().error; assert!( matches!( &error, @@ -715,7 +721,7 @@ fn the_expected_uuid_is_checked_in_the_superblock_before_the_mount() { }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Uuid { expected: e, actual: UUID } if *e == expected), + matches!(&error.error, Error::Uuid { expected: e, actual: UUID } if *e == expected), "{error:?}" ); assert_eq!( @@ -731,7 +737,7 @@ fn the_expected_uuid_is_checked_in_the_superblock_before_the_mount() { fs::write(guest.path("dev/vda"), vec![0; 4096]).unwrap(); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::NotErofs { device } if *device == guest.path("dev/vda")), + matches!(&error.error, Error::NotErofs { device } if *device == guest.path("dev/vda")), "{error:?}" ); assert!(guest.kernel.mounts().is_empty()); @@ -748,12 +754,12 @@ fn a_failed_assembly_unmounts_what_it_mounted_and_reports_what_stays() { let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Mount { mount: Mount::Overlay, error } if error.raw_os_error() == Some(Errno::EINVAL as i32) ), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( guest.kernel.unmounts(), [ @@ -783,14 +789,12 @@ fn a_failed_assembly_unmounts_what_it_mounted_and_reports_what_stays() { os_error(Errno::EBUSY) ) ); - let (_, cleanup) = error.into_parts(); - let failures = cleanup.unwrap(); - let [failure] = failures.failures() else { + let failures = error.cleanup.unwrap(); + let [failure] = failures.failures.as_slice() else { panic!("{failures:?}"); }; - assert_eq!(failure.mount(), &Mount::Distro); - assert_eq!(failure.path(), guest.path("run/nvx/layers/distro")); - assert_eq!(failure.error().raw_os_error(), Some(Errno::EBUSY as i32)); + assert_eq!(failure.mount, Mount::Distro); + assert_eq!(failure.error.raw_os_error(), Some(Errno::EBUSY as i32)); assert_eq!( guest.kernel.unmounts(), [ @@ -808,7 +812,7 @@ fn a_runtime_directory_that_the_overlay_options_reserve_is_refused() { assembly.runtime = runtime.into(); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Runtime { path } if *path == Path::new(runtime)), + matches!(&error.error, Error::Runtime { path } if *path == Path::new(runtime)), "{runtime}: {error:?}" ); } @@ -835,10 +839,10 @@ fn a_runtime_directory_whose_path_is_not_canonical_is_refused() { assembly.runtime = runtime.into(); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Runtime { path } if path.as_os_str() == runtime), + matches!(&error.error, Error::Runtime { path } if path.as_os_str() == runtime), "{runtime}: {error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); } assert!(guest.kernel.calls().is_empty()); assert!(!guest.path("run/nvx/layers").exists()); @@ -867,7 +871,7 @@ fn a_root_mounts_nothing_through_a_symbolic_link() { assembly.runtime = runtime.clone(); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Runtime { path } if *path == runtime), + matches!(&error.error, Error::Runtime { path } if *path == runtime), "{}: {error:?}", runtime.display() ); @@ -884,7 +888,7 @@ fn a_root_mounts_nothing_through_a_symbolic_link() { std::os::unix::fs::symlink(guest.path("elsewhere"), &link).unwrap(); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::MountPoint { path, error: cause } + matches!(&error.error, Error::MountPoint { path, error: cause } if *path == link && cause.raw_os_error() == Some(Errno::ENOTDIR as i32)), "{name}: {error:?}" ); @@ -1006,10 +1010,10 @@ fn a_fresh_upper_s_directories_must_be_missing_or_empty() { fs::create_dir_all(path.join(entry)).unwrap(); let error = guest.assemble_fresh(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::NotFresh { path: refused } if *refused == path), + matches!(&error.error, Error::NotFresh { path: refused } if *refused == path), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( error.to_string(), format!( @@ -1038,7 +1042,7 @@ fn a_fresh_upper_s_directories_must_be_missing_or_empty() { } let error = guest.assemble_fresh(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::MountPoint { path, error } + matches!(&error.error, Error::MountPoint { path, error } if *path == upper && error.raw_os_error() == Some(Errno::ENOTDIR as i32)), "{error:?}" ); @@ -1072,7 +1076,7 @@ fn the_freshness_check_takes_one_entry_of_a_directory_and_opens_none() { sys::tests::create_fifo(&fifo, sys::Mode::from_bits_truncate(0o600)).unwrap(); let error = guest.assemble_fresh(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::NotFresh { path } if *path == upper), + matches!(&error.error, Error::NotFresh { path } if *path == upper), "{error:?}" ); @@ -1096,10 +1100,10 @@ fn a_runtime_directory_that_another_root_holds_is_refused() { let calls = guest.kernel.calls(); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::Reserved { path } if *path == guest.path("run/nvx")), + matches!(&error.error, Error::Reserved { path } if *path == guest.path("run/nvx")), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( error.to_string(), format!( @@ -1153,10 +1157,10 @@ fn a_runtime_directory_that_another_root_holds_is_refused() { let (result, calls) = refused.lock().unwrap().take().unwrap(); let error = result.unwrap_err(); assert!( - matches!(error.error(), Error::Reserved { path } if *path == guest.path("run/nvx")), + matches!(&error.error, Error::Reserved { path } if *path == guest.path("run/nvx")), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!(calls, 0); assert_eq!( guest.kernel.mounts(), @@ -1188,10 +1192,10 @@ fn a_runtime_directory_where_a_file_system_is_mounted_is_refused() { let calls = guest.kernel.calls(); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == distro), + matches!(&error.error, Error::Occupied { path } if *path == distro), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( error.to_string(), format!("a file system is mounted at {} already", distro.display()) @@ -1213,7 +1217,7 @@ fn a_runtime_directory_where_a_file_system_is_mounted_is_refused() { let mounts = guest.kernel.mounts().len(); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == guest.path("run/nvx/rootfs")), + matches!(&error.error, Error::Occupied { path } if *path == guest.path("run/nvx/rootfs")), "{error:?}" ); assert_eq!(guest.kernel.mounts().len(), mounts); @@ -1231,12 +1235,17 @@ fn a_runtime_directory_where_a_file_system_is_mounted_is_refused() { .kernel .fail(Operation::Unmount, shares.clone(), Errno::EBUSY); let error = root.teardown().unwrap_err(); - let failed: Vec<&Path> = error.failures().iter().map(Unmount::path).collect(); - assert_eq!(failed, [shares.as_path()]); + let failed: Vec<&Mount> = error + .failures + .iter() + .map(|failure| &failure.mount) + .collect(); + let aggregate = Mount::Aggregate { tag: "all".into() }; + assert_eq!(failed, [&aggregate]); let mounts = guest.kernel.mounts().len(); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == shares), + matches!(&error.error, Error::Occupied { path } if *path == shares), "{error:?}" ); assert_eq!(guest.kernel.mounts().len(), mounts); @@ -1250,15 +1259,15 @@ fn a_runtime_directory_where_a_file_system_is_mounted_is_refused() { .kernel .fail(Operation::Unmount, mount_point.clone(), Errno::EBUSY); let error = root.teardown().unwrap_err(); - let failed: Vec<(&Mount, &Path)> = error - .failures() + let failed: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount(), failure.path())) + .map(|failure| &failure.mount) .collect(); let mount = Mount::Export { tag: "microvm".into(), }; - assert_eq!(failed, [(&mount, mount_point.as_path())]); + assert_eq!(failed, [&mount]); assert_eq!( error.to_string(), format!( @@ -1269,7 +1278,7 @@ fn a_runtime_directory_where_a_file_system_is_mounted_is_refused() { let mounts = guest.kernel.mounts().len(); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == mount_point), + matches!(&error.error, Error::Occupied { path } if *path == mount_point), "{error:?}" ); assert_eq!(guest.kernel.mounts().len(), mounts); @@ -1297,7 +1306,7 @@ fn a_runtime_directory_where_a_file_system_is_mounted_is_refused() { }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == left), + matches!(&error.error, Error::Occupied { path } if *path == left), "{error:?}" ); assert_eq!(guest.kernel.mounts().len(), mounts); @@ -1409,10 +1418,10 @@ fn a_runtime_directory_where_a_directory_is_bound_is_refused() { ) .unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == distro), + matches!(&error.error, Error::Occupied { path } if *path == distro), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); return; } let directory = tempfile::tempdir().unwrap(); @@ -1529,28 +1538,25 @@ fn a_gpt_disk_s_layers_are_mounted_under_the_overlay_top_first() { // The first partition comes back as its bytes, without the padding after them, and is never // mounted. assert_eq!( - root.first_partition(), + root.first_partition.as_deref(), Some(&b"{\"configuration\": true}"[..]) ); let numbers: Vec = root - .lowers() + .lowers .iter() - .map(|lower| lower.partition().unwrap().number()) + .map(|lower| lower.partition.unwrap().number()) .collect(); assert_eq!(numbers, [2, 3, 4]); - for lower in root.lowers() { - let partition = lower.partition().unwrap(); + for lower in &root.lowers { + let partition = lower.partition.unwrap(); let number = partition.number(); - assert_eq!(lower.mount(), &Mount::Layer { number }); - assert_eq!(lower.device(), guest.path(&format!("dev/vda{number}"))); - assert_eq!( - lower.path(), - guest.path(&format!("run/nvx/layers/p{number}")) - ); + assert_eq!(lower.mount, Mount::Layer { number }); + assert_eq!(lower.device, guest.path(&format!("dev/vda{number}"))); + assert_eq!(lower.path, guest.path(&format!("run/nvx/layers/p{number}"))); assert_eq!(partition.unique_guid(), GptDisk::guid(number)); } - assert_eq!(root.upper(), UpperKind::Scratch); - assert_eq!(root.overlay(), OverlayMode::TrustedLowers); + assert_eq!(root.upper_kind, UpperKind::Scratch); + assert_eq!(root.overlay_mode, OverlayMode::TrustedLowers); root.teardown().unwrap(); assert_eq!( guest.kernel.unmounts(), @@ -1581,7 +1587,7 @@ fn a_gpt_disk_s_layers_are_mounted_under_the_overlay_top_first() { let assembly = guest.gpt_assembly(scratch_upper(Journal::Allowed), OverlayMode::TrustedLowers); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == third), + matches!(&error.error, Error::Occupied { path } if *path == third), "{error:?}" ); assert_eq!(guest.kernel.mounts(), mounts); @@ -1670,9 +1676,9 @@ fn both_overlay_modes_assemble_on_both_uppers_with_their_own_option_sets() { upper.kind() == UpperKind::Scratch, "{case}" ); - assert_eq!(root.upper(), upper.kind(), "{case}"); - assert_eq!(root.overlay(), mode, "{case}"); - assert_eq!(root.first_partition().is_some(), gpt, "{case}"); + assert_eq!(root.upper_kind, upper.kind(), "{case}"); + assert_eq!(root.overlay_mode, mode, "{case}"); + assert_eq!(root.first_partition.as_deref().is_some(), gpt, "{case}"); for directory in ["upper", "work"] { let path = guest.path(&format!("run/nvx/{point}/{directory}")); assert!(path.is_dir(), "{case}: {directory}"); @@ -1739,10 +1745,10 @@ fn a_tmpfs_upper_refuses_a_cap_that_is_not_1_to_90_percent() { }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::TmpfsCap { percent: cap } if *cap == percent), + matches!(&error.error, Error::TmpfsCap { percent: cap } if *cap == percent), "{percent}: {error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( error.to_string(), format!("the tmpfs upper's cap of {percent}% of the guest's memory is not 1 to 90%") @@ -1771,13 +1777,13 @@ fn a_refused_option_set_fails_without_a_fallback() { let error = guest.assemble(&assembly).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Mount { mount: Mount::Overlay, error } if error.raw_os_error() == Some(Errno::EINVAL as i32) ), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); // The overlay was mounted once, with the mode's own option set. let overlays: Vec = guest .kernel @@ -1821,15 +1827,15 @@ fn an_overlay_that_the_kernel_mounts_without_its_mode_in_effect_is_refused_and_u }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::OverlayFallback { requested, effective } + matches!(&error.error, Error::OverlayFallback { requested, effective } if requested == "metacopy=on" && effective == "metacopy=off"), "{error:?}" ); assert_eq!( - error.error().to_string(), + error.error.to_string(), "the kernel mounted the overlay with metacopy=off in place of metacopy=on" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( guest.kernel.unmounts(), below( @@ -1850,7 +1856,7 @@ fn an_overlay_that_the_kernel_mounts_without_its_mode_in_effect_is_refused_and_u }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::OverlayFallback { requested, effective } + matches!(&error.error, Error::OverlayFallback { requested, effective } if requested == "rw" && effective == "ro"), "{error:?}" ); @@ -1876,7 +1882,7 @@ fn options_that_the_kernel_does_not_show_have_their_defaults() { let guest = Guest::new(); guest.show_overlay("rw", UpperKind::Tmpfs, ""); let root = guest.assemble(&untrusted(&guest)).unwrap(); - assert_eq!(root.overlay(), OverlayMode::UntrustedLowers); + assert_eq!(root.overlay_mode, OverlayMode::UntrustedLowers); root.teardown().unwrap(); // Under a kernel that copies up metadata alone by default, the table shows the trusted @@ -1894,7 +1900,7 @@ fn options_that_the_kernel_does_not_show_have_their_defaults() { guest.show_overlay("rw", UpperKind::Tmpfs, ""); let error = guest.assemble(&untrusted(&guest)).unwrap_err(); assert!( - matches!(error.error(), Error::OverlayFallback { requested, effective } + matches!(&error.error, Error::OverlayFallback { requested, effective } if requested == "metacopy=off" && effective == "metacopy=on"), "{error:?}" ); @@ -1907,7 +1913,7 @@ fn options_that_the_kernel_does_not_show_have_their_defaults() { guest.show_overlay("rw", UpperKind::Tmpfs, ""); let error = guest.assemble(&untrusted(&guest)).unwrap_err(); assert!( - matches!(error.error(), Error::OverlayFallback { requested, effective: actual } + matches!(&error.error, Error::OverlayFallback { requested, effective: actual } if requested == "redirect_dir=nofollow" && *actual == format!("redirect_dir={effective}")), "{parameter}: {error:?}" @@ -1933,19 +1939,19 @@ fn effective_options_that_cannot_be_read_fail_the_assembly() { .fail(Operation::Options, rootfs.clone(), Errno::EIO); let error = guest.assemble(&guest.assembly()).unwrap_err(); assert!( - matches!(error.error(), Error::OverlayCheck { path, error } + matches!(&error.error, Error::OverlayCheck { path, error } if *path == rootfs && error.raw_os_error() == Some(Errno::EIO as i32)), "{error:?}" ); assert_eq!( - error.error().to_string(), + error.error.to_string(), format!( "failed to read the overlay's effective options at {}: {}", rootfs.display(), os_error(Errno::EIO) ) ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!( guest.kernel.unmounts(), below( @@ -1974,7 +1980,7 @@ fn effective_options_that_cannot_be_read_fail_the_assembly() { None => Some(Errno::ENOENT as i32), }; assert!( - matches!(error.error(), Error::OverlayCheck { path, error } + matches!(&error.error, Error::OverlayCheck { path, error } if *path == parameter && error.raw_os_error() == expected), "{error:?}" ); @@ -2021,7 +2027,7 @@ fn overlay_options_that_a_mount_cannot_pass_are_refused_before_any_mount() { let length = overlay_data(&runtime, &["layers/distro"], "tmpfs", mode).len(); assert!(length > MOUNT_OPTIONS_LIMIT, "{length}"); assert!( - matches!(error.error(), Error::OverlayOptions { length: actual } if *actual == length), + matches!(&error.error, Error::OverlayOptions { length: actual } if *actual == length), "{error:?}" ); assert_eq!( @@ -2074,7 +2080,7 @@ fn up_to_195_layers_fit_in_the_overlay_s_options_below_run_nvx() { let layers: Vec<&str> = layers.iter().map(String::as_str).collect(); let length = overlay_data(&guest.text("run/nvx"), &layers, "tmpfs", mode).len(); assert!( - matches!(error.error(), Error::OverlayOptions { length: actual } if *actual == length), + matches!(&error.error, Error::OverlayOptions { length: actual } if *actual == length), "{error:?}" ); assert_eq!( @@ -2110,7 +2116,7 @@ fn the_journal_policy_is_checked_in_the_scratch_s_superblock_before_any_mount() } let error = result.unwrap_err(); assert!( - matches!(error.error(), Error::Journal { device } if *device == guest.path("dev/vdb")), + matches!(&error.error, Error::Journal { device } if *device == guest.path("dev/vdb")), "{error:?}" ); assert_eq!( @@ -2132,7 +2138,7 @@ fn the_journal_policy_is_checked_in_the_scratch_s_superblock_before_any_mount() }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::NotExt4 { device } if *device == guest.path("dev/vdb")), + matches!(&error.error, Error::NotExt4 { device } if *device == guest.path("dev/vdb")), "{error:?}" ); assert_eq!( @@ -2150,11 +2156,7 @@ fn a_device_whose_access_mode_is_not_its_role_s_is_refused_before_it_is_flushed( // A writable distro device. let guest = Guest::new(); set_read_only(&guest.base, "vda", false); - let error = guest - .assemble(&guest.assembly()) - .unwrap_err() - .into_parts() - .0; + let error = guest.assemble(&guest.assembly()).unwrap_err().error; assert!( matches!(&error, Error::Access { role: Role::Distro, device, read_only: false } if *device == guest.path("dev/vda")), @@ -2172,11 +2174,7 @@ fn a_device_whose_access_mode_is_not_its_role_s_is_refused_before_it_is_flushed( // A read-only scratch device, checked after the distro device. let guest = Guest::new(); set_read_only(&guest.base, "vdb", true); - let error = guest - .assemble(&guest.assembly()) - .unwrap_err() - .into_parts() - .0; + let error = guest.assemble(&guest.assembly()).unwrap_err().error; assert!( matches!(&error, Error::Access { role: Role::Scratch, device, read_only: true } if *device == guest.path("dev/vdb")), @@ -2196,7 +2194,7 @@ fn a_device_whose_access_mode_is_not_its_role_s_is_refused_before_it_is_flushed( let mut assembly = guest.assembly(); assembly.distro.mmio = SCRATCH; scratch(&mut assembly).mmio = DISTRO; - let error = guest.assemble(&assembly).unwrap_err().into_parts().0; + let error = guest.assemble(&assembly).unwrap_err().error; assert!( matches!(&error, Error::Access { role: Role::Distro, device, read_only: false } if *device == guest.path("dev/vdb")), @@ -2207,11 +2205,7 @@ fn a_device_whose_access_mode_is_not_its_role_s_is_refused_before_it_is_flushed( // An access mode in another form than the kernel's. let guest = Guest::new(); write(&guest.path("sys/class/block/vda/ro"), "2\n"); - let error = guest - .assemble(&guest.assembly()) - .unwrap_err() - .into_parts() - .0; + let error = guest.assemble(&guest.assembly()).unwrap_err().error; assert!( matches!(&error, Error::Device { role: Role::Distro, step: DeviceStep::Access, error, .. } if error.raw_os_error() == Some(Errno::EPROTO as i32)), @@ -2242,20 +2236,12 @@ fn a_tmpfs_upper_is_unmounted_last_and_every_failure_reported() { &["rootfs", "layers/distro", "tmpfs"] ) ); - let failed: Vec<(&Mount, &Path)> = error - .failures() + let failed: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount(), failure.path())) + .map(|failure| &failure.mount) .collect(); - let distro = guest.path("run/nvx/layers/distro"); - let tmpfs = guest.path("run/nvx/tmpfs"); - assert_eq!( - failed, - [ - (&Mount::Distro, distro.as_path()), - (&Mount::Tmpfs, tmpfs.as_path()) - ] - ); + assert_eq!(failed, [&Mount::Distro, &Mount::Tmpfs]); assert_eq!( error.to_string(), format!( @@ -2317,14 +2303,14 @@ fn a_scratch_whose_sync_fails_stays_mounted_and_the_failure_is_reported() { below(&guest.text("run/nvx"), &["rootfs", "layers/distro"]) ); assert!(guest.kernel.mount_at(&scratch).is_some()); - let failed: Vec<(&Mount, &Path)> = error - .failures() + let failed: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount(), failure.path())) + .map(|failure| &failure.mount) .collect(); - assert_eq!(failed, [(&Mount::Scratch, scratch.as_path())]); + assert_eq!(failed, [&Mount::Scratch]); assert_eq!( - error.failures()[0].error().raw_os_error(), + error.failures[0].error.raw_os_error(), Some(Errno::EIO as i32) ); assert_eq!( @@ -2404,7 +2390,7 @@ fn a_gpt_disk_that_differs_from_the_expected_identities_is_refused_before_any_mo }; let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Disk { device, error: DeviceError::Table(_) } + matches!(&error.error, Error::Disk { device, error: DeviceError::Table(_) } if *device == guest.path("dev/vda")), "{error:?}" ); @@ -2428,7 +2414,7 @@ fn a_gpt_disk_without_a_layer_is_refused_before_any_mount() { let error = guest.assemble(&assembly).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Disk { error: DeviceError::Table(GptError::PartitionCount(1)), .. @@ -2467,7 +2453,7 @@ fn a_layer_that_is_not_ext4_or_whose_journal_needs_a_recovery_is_refused_before_ let assembly = guest.gpt_assembly(TMPFS, OverlayMode::TrustedLowers); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Recovery { number: 3, device } + matches!(&error.error, Error::Recovery { number: 3, device } if *device == guest.path("dev/vda3")), "{error:?}" ); @@ -2489,7 +2475,7 @@ fn a_layer_that_is_not_ext4_or_whose_journal_needs_a_recovery_is_refused_before_ let assembly = guest.gpt_assembly(TMPFS, OverlayMode::TrustedLowers); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::NotExt4Layer { number: 2, device } + matches!(&error.error, Error::NotExt4Layer { number: 2, device } if *device == guest.path("dev/vda2")), "{error:?}" ); @@ -2511,7 +2497,7 @@ fn a_layer_that_is_not_ext4_or_whose_journal_needs_a_recovery_is_refused_before_ let error = guest.assemble(&assembly).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Device { role: Role::Distro, step: DeviceStep::Superblock, @@ -2536,7 +2522,7 @@ fn a_missing_partition_fails_at_once_before_any_mount() { let assembly = guest.gpt_assembly(TMPFS, OverlayMode::TrustedLowers); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Partition { number: 3, node } + matches!(&error.error, Error::Partition { number: 3, node } if *node == guest.path("dev/vda3")), "{error:?}" ); @@ -2814,7 +2800,7 @@ fn the_first_partition_is_returned_as_opaque_bytes_within_its_bound() { first_partition_bound: disk.first_partition_blocks() * BLOCK, }; let root = guest.assemble(&assembly).unwrap(); - assert_eq!(root.first_partition(), Some(expected)); + assert_eq!(root.first_partition.as_deref(), Some(expected)); assert_eq!( guest.kernel.mounts(), below(&guest.text("run/nvx"), &["layers/p2", "tmpfs", "rootfs"]) @@ -2833,7 +2819,7 @@ fn the_first_partition_is_returned_as_opaque_bytes_within_its_bound() { let error = guest.assemble(&assembly).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::FirstPartition { device, error: GptError::FirstPartitionTooLarge { @@ -2875,19 +2861,12 @@ fn a_teardown_unmounts_the_layers_top_first_and_reports_every_failure() { &["rootfs", "layers/p4", "layers/p3", "layers/p2", "tmpfs"] ) ); - let failed: Vec<(&Mount, &Path)> = error - .failures() + let failed: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount(), failure.path())) + .map(|failure| &failure.mount) .collect(); - let (third, tmpfs) = (guest.path("run/nvx/layers/p3"), guest.path("run/nvx/tmpfs")); - assert_eq!( - failed, - [ - (&Mount::Layer { number: 3 }, third.as_path()), - (&Mount::Tmpfs, tmpfs.as_path()) - ] - ); + assert_eq!(failed, [&Mount::Layer { number: 3 }, &Mount::Tmpfs]); assert_eq!( error.to_string(), format!( @@ -2909,7 +2888,7 @@ fn a_teardown_unmounts_the_layers_top_first_and_reports_every_failure() { let error = guest.assemble(&assembly).unwrap_err(); assert!( matches!( - error.error(), + &error.error, Error::Mount { mount: Mount::Layer { number: 3 }, .. @@ -2917,7 +2896,7 @@ fn a_teardown_unmounts_the_layers_top_first_and_reports_every_failure() { ), "{error:?}" ); - assert!(error.cleanup().is_none()); + assert!(error.cleanup.is_none()); assert_eq!(guest.kernel.unmounts(), [guest.text("run/nvx/layers/p2")]); } @@ -2942,7 +2921,7 @@ fn a_layer_that_a_failed_unmount_left_keeps_the_runtime_directory_occupied() { for assembly in [erofs, assembly] { let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == top), + matches!(&error.error, Error::Occupied { path } if *path == top), "{error:?}" ); assert_eq!(guest.kernel.mounts().len(), mounts); @@ -2965,7 +2944,7 @@ fn a_layer_that_a_failed_unmount_left_keeps_the_runtime_directory_occupied() { let assembly = guest.gpt_assembly(TMPFS, OverlayMode::TrustedLowers); let error = guest.assemble(&assembly).unwrap_err(); assert!( - matches!(error.error(), Error::Occupied { path } if *path == distro), + matches!(&error.error, Error::Occupied { path } if *path == distro), "{error:?}" ); assert_eq!(guest.kernel.mounts().len(), mounts); @@ -3115,7 +3094,7 @@ fn shares_are_mounted_restricted_and_torn_down_first_in_reverse_order() { assert_eq!(mode("run/nvx/rootfs/etc/app"), 0o755); assert_eq!(mode("run/nvx/rootfs/etc/app/config"), 0o644); assert_eq!(guest.kernel.mount_at(&guest.path("run/nvx/staging")), None); - assert!(!root.is_latched()); + assert!(!root.latched); root.teardown().unwrap(); assert_eq!( @@ -3203,7 +3182,7 @@ fn refused_shares_mount_nothing() { ); assert_eq!(guest.kernel.mounts().len(), mounts); // The refusals of requests latch nothing. - assert!(!root.is_latched()); + assert!(!root.latched); // A share that fails to mount claims no target, and latches the root. guest.kernel.fail( @@ -3220,7 +3199,7 @@ fn refused_shares_mount_nothing() { ) ); assert_eq!(targets(&root), ["/srv"]); - assert!(root.is_latched()); + assert!(root.latched); let error = root.mount_share("t", Path::new("/t"), rw).unwrap_err(); assert!( matches!( @@ -3261,7 +3240,7 @@ fn a_root_has_one_aggregate_and_one_that_failed_part_of_the_way_latches_it() { ], ); assert_eq!(first.is_ok(), bound.len() == 2, "{first:?}"); - assert_eq!(root.is_latched(), bound.len() == 1); + assert_eq!(root.latched, bound.len() == 1); let mounts = guest.kernel.mounts().len(); let error = root .mount_aggregate("more", rw, &[child("c", "/c", rw, directory)]) @@ -3431,24 +3410,16 @@ fn a_failed_unmount_is_reported_and_the_later_ones_still_attempted() { Errno::EBUSY, ); let error = root.teardown().unwrap_err(); - let failures: Vec<_> = error - .failures() + let failures: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount().clone(), failure.path().to_owned())) + .map(|failure| &failure.mount) .collect(); - assert_eq!( - failures, - [ - ( - Mount::Share { - tag: "srv".into(), - target: "/srv".into() - }, - guest.path("run/nvx/rootfs/srv") - ), - (Mount::Overlay, guest.path("run/nvx/rootfs")), - ] - ); + let share = Mount::Share { + tag: "srv".into(), + target: "/srv".into(), + }; + assert_eq!(failures, [&share, &Mount::Overlay]); assert_eq!( error.to_string(), format!( @@ -3494,7 +3465,7 @@ fn a_share_through_a_link_or_onto_another_mount_is_refused_and_latches_the_root( ); assert_eq!(guest.kernel.unmounts(), [guest.text("run/nvx/staging")]); assert!(!guest.path("elsewhere/new").exists()); - assert!(root.is_latched()); + assert!(root.latched); root.teardown().unwrap(); // Another mount in the root, where nothing is created. @@ -3527,21 +3498,17 @@ fn a_share_through_a_link_or_onto_another_mount_is_refused_and_latches_the_root( root.mount_share("t", Path::new("/link/new"), rw) .unwrap_err(); let error = root.teardown().unwrap_err(); - let failed: Vec<_> = error - .failures() + let failed: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount().clone(), failure.path().to_owned())) + .map(|failure| &failure.mount) .collect(); - assert_eq!( - failed, - [( - Mount::Share { - tag: "t".into(), - target: "/link/new".into() - }, - staging.clone() - )] - ); + let share = Mount::Share { + tag: "t".into(), + target: "/link/new".into(), + }; + assert_eq!(failed, [&share]); + assert!(guest.kernel.mount_at(&staging).is_some()); // A `staging` where a file system is mounted already is refused before the device mounts. let guest = Guest::new(); @@ -3569,9 +3536,6 @@ fn a_share_or_a_mapping_that_a_rename_moved_and_that_stays_is_the_teardown_s() { let mut root = guest.assemble(&guest.assembly()).unwrap(); let rootfs = guest.path("run/nvx/rootfs"); fs::create_dir_all(rootfs.join("data/x")).unwrap(); - let export = guest.path("hostfs/root"); - fs::create_dir_all(export.join("0/src")).unwrap(); - guest.kernel.add_mount(&export); // A rename between the walk and the attach moves the target's directory, and the detach // of what went there fails. let renamed = rootfs.clone(); @@ -3582,21 +3546,21 @@ fn a_share_or_a_mapping_that_a_rename_moved_and_that_stays_is_the_teardown_s() { guest .kernel .fail(Operation::Detach, moved.clone(), Errno::EBUSY); + let rw = Access::ReadWrite; let (error, mount) = if share { - let error = root - .mount_share("t", Path::new("/data/x"), Access::ReadWrite) - .unwrap_err(); + let error = root.mount_share("t", Path::new("/data/x"), rw).unwrap_err(); let mount = Mount::Share { tag: "t".into(), target: "/data/x".into(), }; (error, mount) } else { - let source = SourceRoot::open(&export).unwrap(); - let mapping = - crate::guest::mappings::Mapping::new("0/src", "/data/x", Access::ReadOnly).unwrap(); - let error = root.map(&source, &[mapping]).unwrap_err(); - let mount = Mount::Mapping { + // A child of an aggregate is a mapping of the root's mapper. + export(&guest, &[("x", ShareKind::Directory)]); + let children = [child("x", "/data/x", rw, ShareKind::Directory)]; + let error = root.mount_aggregate("all", rw, &children).unwrap_err(); + let mount = Mount::Child { + name: "x".into(), target: "/data/x".into(), }; (error, mount) @@ -3611,7 +3575,7 @@ fn a_share_or_a_mapping_that_a_rename_moved_and_that_stays_is_the_teardown_s() { ), "{error:?}" ); - assert!(root.is_latched()); + assert!(root.latched); assert!(guest.kernel.mount_at(&moved).is_some()); assert_eq!(guest.kernel.mount_at(&guest.path("run/nvx/staging")), None); // The teardown detaches it first, wherever it is, or reports it. @@ -3629,110 +3593,18 @@ fn a_share_or_a_mapping_that_a_rename_moved_and_that_stays_is_the_teardown_s() { Ok(()) => torn.unwrap(), Err(busy) => { let error = torn.unwrap_err(); - let failures: Vec<_> = error - .failures() + let failures: Vec<&Mount> = error + .failures .iter() - .map(|failure| (failure.mount().clone(), failure.path().to_owned())) + .map(|failure| &failure.mount) .collect(); - assert_eq!(failures, [(mount, rootfs.join("data/x"))]); - assert_eq!( - error.failures()[0].error().raw_os_error(), - Some(busy as i32) - ); + assert_eq!(failures, [&mount]); + assert_eq!(error.failures[0].error.raw_os_error(), Some(busy as i32)); } } } } -#[test] -fn mappings_apply_beneath_the_overlay_in_order_and_go_first_at_the_teardown() { - let guest = Guest::new(); - let mut root = guest.assemble(&guest.assembly()).unwrap(); - let export = guest.path("hostfs/root"); - fs::create_dir_all(export.join("0/src")).unwrap(); - fs::write(export.join("0/notes.txt"), b"notes").unwrap(); - guest.kernel.add_mount(&export); - let source = SourceRoot::open(&export).unwrap(); - let calls = guest.kernel.calls().len(); - // Zero mappings do no work. - root.map(&source, &[]).unwrap(); - assert_eq!(guest.kernel.calls().len(), calls); - let mapping = |source: &str, target: &str, access| { - crate::guest::mappings::Mapping::new(source, target, access).unwrap() - }; - root.map( - &source, - &[ - mapping("0/src", "/workspace", Access::ReadWrite), - mapping("0/notes.txt", "/home/user/notes", Access::ReadOnly), - ], - ) - .unwrap(); - let mut expected = attach_calls( - &export.join("0/src"), - &guest.path("run/nvx/rootfs/workspace"), - Access::ReadWrite, - ); - expected.extend(attach_calls( - &export.join("0/notes.txt"), - &guest.path("run/nvx/rootfs/home/user/notes"), - Access::ReadOnly, - )); - assert_eq!(guest.kernel.calls()[calls..], expected); - assert_eq!(targets(&root), ["/workspace", "/home/user/notes"]); - // A share may not shadow a mapping, nor be inside one. - let error = root - .mount_share("t", Path::new("/workspace/inner"), Access::ReadWrite) - .unwrap_err(); - assert!( - matches!(&error, Error::Share(ShareRefusal::Overlap { claimed, .. }) - if *claimed == Path::new("/workspace")), - "{error:?}" - ); - // NVX's container root reserves its runtime's targets; the refusal latches the root. - let error = root - .map(&source, &[mapping("0/src", "/etc", Access::ReadOnly)]) - .unwrap_err(); - assert!( - matches!( - &error, - Error::Mapping { mount: Mount::Mapping { target }, error: MappingError::Reserved } - if *target == Path::new("/etc") - ), - "{error:?}" - ); - assert_eq!( - error.to_string(), - "failed to mount the mapping at /etc: the runtime reserves the target" - ); - let error = root - .map(&source, &[mapping("0/src", "/srv", Access::ReadOnly)]) - .unwrap_err(); - assert!( - matches!( - &error, - Error::Mapping { - error: MappingError::Latched, - .. - } - ), - "{error:?}" - ); - root.map(&source, &[]).unwrap(); - assert!(!guest.path("run/nvx/rootfs/srv").exists()); - root.teardown().unwrap(); - assert_eq!( - guest.kernel.unmounts(), - [ - guest.text("run/nvx/rootfs/home/user/notes"), - guest.text("run/nvx/rootfs/workspace"), - guest.text("run/nvx/rootfs"), - guest.text("run/nvx/layers/distro"), - guest.text("run/nvx/scratch"), - ] - ); -} - #[test] fn a_root_in_run_nvx_mounts_the_host_export_where_its_boot_token_names_it() { assert_eq!( @@ -3746,7 +3618,7 @@ fn a_root_in_run_nvx_mounts_the_host_export_where_its_boot_token_names_it() { } #[test] -fn the_host_export_is_mounted_out_of_the_root_and_unmounted_after_its_mappings() { +fn the_host_export_is_mounted_out_of_the_root_and_unmounted_after_the_live_shares() { let restricted = MountFlags::MS_NOSUID | MountFlags::MS_NODEV; for (access, flags) in [ (Access::ReadOnly, restricted | MountFlags::MS_RDONLY), @@ -3776,33 +3648,7 @@ fn the_host_export_is_mounted_out_of_the_root_and_unmounted_after_its_mappings() fs::read_dir(guest.path("run/nvx/rootfs")).unwrap().count(), 0 ); - assert!(!root.is_latched()); - - // Its root is the source of the caller's mappings: a directory child, `0`, and a file - // child, `1`, of the aggregate. - fs::create_dir_all(mount_point.join("0/src")).unwrap(); - fs::write(mount_point.join("1"), b"notes").unwrap(); - let mapped = guest.kernel.calls().len(); - root.map( - &source, - &[ - Mapping::new("0/src", "/workspace", Access::ReadWrite).unwrap(), - Mapping::new("1", "/home/user/notes", Access::ReadOnly).unwrap(), - ], - ) - .unwrap(); - let mut expected = attach_calls( - &mount_point.join("0/src"), - &guest.path("run/nvx/rootfs/workspace"), - Access::ReadWrite, - ); - expected.extend(attach_calls( - &mount_point.join("1"), - &guest.path("run/nvx/rootfs/home/user/notes"), - Access::ReadOnly, - )); - assert_eq!(guest.kernel.calls()[mapped..], expected); - assert_eq!(targets(&root), ["/workspace", "/home/user/notes"]); + assert!(!root.latched); // A root mounts one export, since every export is mounted at the same place; refusing // another latches nothing. @@ -3814,7 +3660,7 @@ fn the_host_export_is_mounted_out_of_the_root_and_unmounted_after_its_mappings() ); assert_eq!(error.to_string(), "the root has a host export already"); assert_eq!(guest.kernel.mounts().len(), mounts); - assert!(!root.is_latched()); + assert!(!root.latched); // An aggregate live share, mounted later, is unmounted before the export. export(&guest, &[("a", ShareKind::Directory)]); @@ -3826,16 +3672,14 @@ fn the_host_export_is_mounted_out_of_the_root_and_unmounted_after_its_mappings() ) .unwrap(); - // The teardown unmounts the live shares and mappings, those from the export included, - // then the aggregate and the export, the later first, then the assembly's mounts. + // The teardown unmounts the live shares, then the aggregate and the export, the later + // first, then the assembly's mounts, once the export's root is closed. drop(source); root.teardown().unwrap(); assert_eq!( guest.kernel.unmounts(), [ guest.text("run/nvx/rootfs/data/a"), - guest.text("run/nvx/rootfs/home/user/notes"), - guest.text("run/nvx/rootfs/workspace"), guest.text("run/nvx/shares"), guest.text("run/nvx/hostfs/root"), guest.text("run/nvx/rootfs"), @@ -3865,7 +3709,7 @@ fn a_refused_tag_creates_and_mounts_nothing_and_a_failed_export_latches_the_root assert_eq!(guest.kernel.calls(), calls); assert!(!guest.path("run/nvx/hostfs").exists()); // The refusals of requests latch nothing. - assert!(!root.is_latched()); + assert!(!root.latched); // A device that fails to mount latches the root, and leaves nothing to unmount. let mount_point = guest.path("run/nvx/hostfs/root"); @@ -3888,7 +3732,7 @@ fn a_refused_tag_creates_and_mounts_nothing_and_a_failed_export_latches_the_root os_error(Errno::ENODEV) ) ); - assert!(root.is_latched()); + assert!(root.latched); // A latched root refuses the export, as it refuses every share and mapping. guest.kernel.restore(Operation::Mount, &mount_point); @@ -3948,7 +3792,7 @@ fn the_host_export_is_mounted_through_no_symbolic_link() { 0, "{link}" ); - assert!(root.is_latched(), "{link}"); + assert!(root.latched, "{link}"); root.teardown().unwrap(); } } @@ -3984,7 +3828,7 @@ fn a_hostfs_directory_that_is_there_already_must_be_private() { assert_eq!(guest.kernel.mounts().len(), mounts, "{mode:o}"); assert!(!guest.path("run/nvx/hostfs/root").exists(), "{mode:o}"); assert_eq!(fs::metadata(&hostfs).unwrap().mode() & 0o7777, mode); - assert!(root.is_latched(), "{mode:o}"); + assert!(root.latched, "{mode:o}"); root.teardown().unwrap(); } @@ -3998,7 +3842,7 @@ fn a_hostfs_directory_that_is_there_already_must_be_private() { drop(root.mount_export("microvm", Access::ReadWrite).unwrap()); let mode = fs::metadata(guest.path("run/nvx/hostfs")).unwrap().mode() & 0o7777; assert_eq!(mode, 0o700); - assert!(!root.is_latched()); + assert!(!root.latched); root.teardown().unwrap(); let unmounts = [ guest.text("run/nvx/hostfs/root"), @@ -4038,3 +3882,43 @@ fn a_dropped_root_is_torn_down_once() { .unwrap(); assert_eq!(guest.kernel.unmounts().len(), 3); } + +/// Compiles a layered root's steps, as the module's example showed them, without assembling +/// anything: a GPT root over a tmpfs upper, a live share, the host export that the kernel command +/// line names, if any, and the teardown. +#[test] +fn a_layered_root_s_steps_compile() { + use crate::protocol::export::HostExport; + use crate::protocol::root::DISTRO_MMIO_BASE; + + let _steps = || -> Result<(), Box> { + let mut root = assemble(&Assembly { + runtime: "/run/nvx".into(), + distro: Device { + mmio: DISTRO_MMIO_BASE, + sectors: None, + }, + image: Image::Gpt { + expected: Expected::new() + .disk_guid("8d2f3a4e-1c5b-4e7a-9f60-2b3c4d5e6f70".parse()?), + first_partition_bound: 64 * 1024, + }, + upper: Upper::Tmpfs { + max_memory_percent: Some(50), + }, + overlay: OverlayMode::UntrustedLowers, + timeout: Duration::from_secs(30), + })?; + root.mount_share("workspace", Path::new("/workspace"), Access::ReadWrite)?; + // The host export that the kernel command line names, if any, is the source of the + // caller's mappings, which the caller's own mapper applies. + if let Some(export) = HostExport::parse(&fs::read("/proc/cmdline")?)? { + let source = root.mount_export(&export.tag, export.mode.into())?; + // Dropping the export's root lets the teardown unmount the export. + drop(source); + } + // Run the workload in the root at `root.path()`; then: + root.teardown()?; + Ok(()) + }; +} diff --git a/nvx-rs/src/guest/root/tests/privileged.rs b/nvx-rs/src/guest/root/tests/privileged.rs index 46f442635..169018e6f 100644 --- a/nvx-rs/src/guest/root/tests/privileged.rs +++ b/nvx-rs/src/guest/root/tests/privileged.rs @@ -7,10 +7,10 @@ //! They cover what the fake kernel cannot: the runtime's file systems and their options, the //! device nodes, with their mode whatever the umask, and the links, the time status's read-only //! bind, the undo of a call that fails part of the way, a second call's refusal, the teardown's -//! unmounts, and the order of the runtime and the mappings beneath `/run`: a call after a mapping -//! is refused and leaves it reachable, and a mapping after the runtime is on the `/run` tmpfs. -//! They also cover a switch into the root, in a thread with root and working directories of its -//! own, after which the root's mounts stay and descriptors opened before it still map. +//! unmounts, and mappings beneath `/run` after the runtime, which a mapper of the root and of its +//! `/run` tmpfs, as nvx-init makes one, puts on that tmpfs. They also cover a switch into the +//! root, in a thread with root and working directories of its own, after which the root's mounts +//! stay and descriptors opened before it still map. use std::os::unix::fs::FileTypeExt; @@ -294,48 +294,6 @@ fn a_call_that_fails_part_of_the_way_unmounts_what_it_mounted() { assert_eq!(mounts_below(&base), Vec::::new()); } -#[test] -#[ignore = "needs root and Linux 6.8 or later"] -fn a_call_after_a_mapping_is_refused_and_leaves_it_reachable() { - let Some(scene) = Scene::new() else { - return; - }; - let base = scene.path("root"); - let app = base.join("run/app"); - let mut root = scene.root(); - let mapping = Mapping::new("app", "/run/app", Access::ReadOnly).unwrap(); - root.map(&sources(&scene), &[mapping]).unwrap(); - // The `/run` tmpfs would hide the mapping, which the teardown could then not unmount. Every - // step runs before the one check, and the scene detaches what a failed one leaves. - let first = root.mount_runtime(Some(&scene.path("time"))); - let read = fs::read(app.join("data")).map_err(|error| error.kind()); - let mounts = mounts_below(&base); - let second = root.mount_runtime(None); - let torn = root.teardown().map_err(|error| error.to_string()); - let refused = Err(format!( - "a file system is mounted at {} already", - app.display() - )); - assert_eq!( - ( - first.map_err(|error| error.to_string()), - read, - mounts, - second.map_err(|error| error.to_string()), - torn, - mounts_below(&base), - ), - ( - refused.clone(), - Ok(b"mapped".to_vec()), - vec![app.clone()], - refused, - Ok(()), - Vec::new(), - ) - ); -} - #[test] #[ignore = "needs root and Linux 6.8 or later"] fn mappings_beneath_run_after_the_runtime_are_on_its_tmpfs_and_go_with_it() { @@ -346,12 +304,16 @@ fn mappings_beneath_run_after_the_runtime_are_on_its_tmpfs_and_go_with_it() { let path = |relative: &str| base.join(relative); let mut root = scene.root(); root.mount_runtime(Some(&scene.path("time"))).unwrap(); + // A mapper beneath the root, whose own mounts include the `/run` tmpfs, as nvx-init makes it. + let mut target = TargetRoot::open(&base).unwrap(); + target.add_mount(&path("run")).unwrap(); + let mut mapper = Mapper::new(target, Reserved::container_root()); let mappings = [ Mapping::new("app", "/run/app", Access::ReadOnly).unwrap(), Mapping::new("state", "/run/nvx/state", Access::ReadOnly).unwrap(), ]; // Every step runs before the one check, and the scene detaches what a failed one leaves. - let mapped = root.map(&sources(&scene), &mappings); + let mapped = mapper.apply_with(&mut SystemKernel, &sources(&scene), &mappings); let options = ["run/app", "run/nvx/state"] .map(|relative| shown(&path(relative)).map(|(_, options)| options)); let read = ["run/app/data", "run/nvx/state/data", "run/nvx/time/state"] @@ -359,6 +321,9 @@ fn mappings_beneath_run_after_the_runtime_are_on_its_tmpfs_and_go_with_it() { let mounts = mounts_below(&base); let second = root.mount_runtime(None); let kept = mounts_below(&base) == mounts; + // The mapper's mappings go before the runtime's mounts. + let unmapped = mapper.unmount_with(&mut SystemKernel); + drop(mapper); let torn = root.teardown().map_err(|error| error.to_string()); // The mount points that the mapper created went with the `/run` tmpfs. let created = path("run/app").exists(); @@ -370,6 +335,7 @@ fn mappings_beneath_run_after_the_runtime_are_on_its_tmpfs_and_go_with_it() { read, second.map_err(|error| error.to_string()), kept, + unmapped, torn, mounts_below(&base), created, @@ -387,6 +353,7 @@ fn mappings_beneath_run_after_the_runtime_are_on_its_tmpfs_and_go_with_it() { path("proc").display() )), true, + Vec::new(), Ok(()), Vec::new(), false, @@ -409,11 +376,14 @@ fn a_switched_root_keeps_its_mounts_and_maps_through_earlier_descriptors() { fs::write(source.join("late"), b"late").unwrap(); let mut root = scene.root(); root.mount_runtime(None).unwrap(); - // The root's own mapper, a target root, and a source root are opened before the switch. + // A mapper's target root and a source root are opened before the switch, and the mapper maps + // before it too. let sources = SourceRoot::open(&source).unwrap(); + let mut mapper = Mapper::new(TargetRoot::open(&base).unwrap(), Reserved::container_root()); let early = Mapping::new("early", "/srv/early", Access::ReadOnly).unwrap(); - root.map(&sources, &[early]).unwrap(); - let target = TargetRoot::open(&base).unwrap(); + mapper + .apply_with(&mut SystemKernel, &sources, &[early]) + .unwrap(); // The thread that switches has root and working directories of its own, so the test's // thread still reaches the temporary directory afterwards. @@ -426,15 +396,16 @@ fn a_switched_root_keeps_its_mounts_and_maps_through_earlier_descriptors() { assert!(!source.exists()); assert_eq!(fs::read("/srv/early/data").unwrap(), b"early"); - // A mapper maps beneath the target root, from the source root, which no path + // The mapper maps beneath the target root, from the source root, which no path // reaches any more. - let mut mapper = Mapper::new(target, Reserved::container_root()); let late = Mapping::new("late", "/srv/late", Access::ReadOnly).unwrap(); - mapper.apply(&sources, &[late]).unwrap(); + mapper + .apply_with(&mut SystemKernel, &sources, &[late]) + .unwrap(); assert_eq!(fs::read("/srv/late").unwrap(), b"late"); // A second switch and the teardown are refused, and the drop that follows unmounts - // nothing: the runtime's mounts and the root's mapping stay. + // nothing: the runtime's mounts and the mappings stay. let mounts = mounts_below(Path::new("/")); for point in ["/proc", "/dev/pts", "/run", "/srv/early", "/srv/late"] { assert!( @@ -573,7 +544,7 @@ fn view_failure_after_the_machine_id_bind_leaves_it_owned_for_teardown() { crate::guest::view::apply(&mut root, &view, Some("10.0.0.1".parse().unwrap())).unwrap_err(); assert_eq!(error.kind(), crate::ErrorKind::Io); assert!(shown(&base.join("etc/machine-id")).is_some()); - assert!(root.is_latched()); + assert!(root.latched); root.teardown().unwrap(); assert!(mounts_below(&base).is_empty()); assert_eq!( diff --git a/nvx-rs/src/guest/root/tests/runtime.rs b/nvx-rs/src/guest/root/tests/runtime.rs index 4800cc3c0..b50331915 100644 --- a/nvx-rs/src/guest/root/tests/runtime.rs +++ b/nvx-rs/src/guest/root/tests/runtime.rs @@ -2,7 +2,7 @@ //! mount, the runtime's mounts in their order and with their options, the time status's //! read-only bind or none, the mount points that it creates or refuses, the undo of a call that //! fails at any of its steps, the teardown's order, the refusal of a call after a share or a -//! mapping, and the shares and mappings beneath `/run` after the runtime. +//! child of an aggregate, and the shares and children beneath `/run` after the runtime. use super::*; @@ -64,15 +64,6 @@ fn occupied(path: &Path) -> Result<(), String> { )) } -/// Returns the root of the mappings' sources beside the runtime directory, `hostfs/root`, a mount -/// of its own, with the directory `0/src`. -fn source(guest: &Guest) -> SourceRoot { - let export = guest.path("hostfs/root"); - fs::create_dir_all(export.join("0/src")).unwrap(); - guest.kernel.add_mount(&export); - SourceRoot::open(&export).unwrap() -} - /// Returns a recorded new mount of `kind` with `parameters` and `attributes`. fn new_mount(kind: &str, parameters: &[&str], attributes: u64) -> Call { Call::NewMount { @@ -416,10 +407,9 @@ fn a_runtime_mount_whose_detach_fails_is_owned_until_teardown() { if persistent { let error = torn.unwrap_err(); assert!( - error.failures().iter().any(|failure| { - failure.mount() == &Mount::Runtime(runtime) - && failure.path() == original - && failure.error().raw_os_error() == Some(Errno::EBUSY as i32) + error.failures.iter().any(|failure| { + failure.mount == Mount::Runtime(runtime) + && failure.error.raw_os_error() == Some(Errno::EBUSY as i32) }), "{runtime:?}: {error:?}" ); @@ -498,10 +488,9 @@ fn a_time_status_bind_whose_lookup_and_detach_fail_reports_its_teardown_failure( root.mount_runtime(Some(&guest.path("time"))).unwrap_err(); let error = root.teardown().unwrap_err(); assert!( - error.failures().iter().any(|failure| { - failure.mount() == &Mount::Runtime(RuntimeMounts::TimeStatus) - && failure.path() == time - && failure.error().raw_os_error() == Some(Errno::EBUSY as i32) + error.failures.iter().any(|failure| { + failure.mount == Mount::Runtime(RuntimeMounts::TimeStatus) + && failure.error.raw_os_error() == Some(Errno::EBUSY as i32) }), "{error:?}" ); @@ -509,30 +498,22 @@ fn a_time_status_bind_whose_lookup_and_detach_fail_reports_its_teardown_failure( } #[test] -fn a_call_after_a_share_a_child_or_a_mapping_is_refused_before_it_changes_anything() { +fn a_call_after_a_share_or_a_child_is_refused_before_it_changes_anything() { let rw = Access::ReadWrite; // The `/run` tmpfs would hide one beneath `/run` from the teardown, and the runtime comes // first whatever the target. - for (kind, target) in [ - ("share", "/run/app"), - ("child", "/run/app"), - ("mapping", "/srv"), - ] { + for (kind, target) in [("share", "/run/app"), ("child", "/run/app")] { let (guest, mut root) = assembled(); let path = guest.path("run/nvx/rootfs").join(&target[1..]); let mut unmounts = vec![path.display().to_string()]; match kind { "share" => root.mount_share("app", Path::new(target), rw).unwrap(), - "child" => { + _ => { export(&guest, &[("app", ShareKind::Directory)]); let children = [child("app", target, rw, ShareKind::Directory)]; root.mount_aggregate("all", rw, &children).unwrap(); unmounts.push(guest.text("run/nvx/shares")); } - _ => { - let mapping = Mapping::new("0/src", target, rw).unwrap(); - root.map(&source(&guest), &[mapping]).unwrap(); - } } let calls = guest.kernel.calls().len(); let unmounted = guest.kernel.unmounts().len(); @@ -563,30 +544,38 @@ fn a_call_after_a_share_a_child_or_a_mapping_is_refused_before_it_changes_anythi } #[test] -fn shares_and_mappings_beneath_run_after_the_runtime_are_on_its_tmpfs_and_unmounted_first() { +fn shares_and_children_beneath_run_after_the_runtime_are_on_its_tmpfs_and_unmounted_first() { let (guest, mut root) = assembled(); root.mount_runtime(Some(&guest.path("time"))).unwrap(); let unmounted = guest.kernel.unmounts().len(); // The mapper creates what the targets lack on the `/run` tmpfs, one of its own mounts. let shared = root.mount_share("app", Path::new("/run/app"), Access::ReadWrite); - let mapping = Mapping::new("0/src", "/run/nvx/state", Access::ReadOnly).unwrap(); - let mapped = root.map(&source(&guest), &[mapping]); + export(&guest, &[("state", ShareKind::Directory)]); + let children = [child( + "state", + "/run/nvx/state", + Access::ReadOnly, + ShareKind::Directory, + )]; + let bound = root.mount_aggregate("all", Access::ReadOnly, &children); let tracked = targets(&root); // A second call is still refused for the runtime's own mounts first. let again = root.mount_runtime(None); let torn = root.teardown().map_err(|error| error.to_string()); let observed = ( shared.map_err(|error| error.to_string()), - mapped.map_err(|error| error.to_string()), + bound.map_err(|error| error.to_string()), tracked, again.map_err(|error| error.to_string()), torn, guest.kernel.unmounts()[unmounted..].to_vec(), ); - // The teardown unmounts the mapper's mounts, the later first, before the runtime's. + // The teardown unmounts the mapper's mounts, the later first, then the aggregate, before the + // runtime's. let mut unmounts = vec![ guest.text("run/nvx/rootfs/run/nvx/state"), guest.text("run/nvx/rootfs/run/app"), + guest.text("run/nvx/shares"), ]; unmounts.extend(reversed(&guest, &RUNTIME)); let expected = ( diff --git a/nvx-rs/src/guest/root/tests/switch.rs b/nvx-rs/src/guest/root/tests/switch.rs index ac9b45b71..9be725b0f 100644 --- a/nvx-rs/src/guest/root/tests/switch.rs +++ b/nvx-rs/src/guest/root/tests/switch.rs @@ -46,7 +46,7 @@ fn a_root_switches_once_and_its_teardown_then_unmounts_nothing() { // The teardown refuses, and neither it nor the drop that follows unmounts anything. let error = root.teardown().unwrap_err(); assert!(error.is_switched(), "{error:?}"); - assert!(error.failures().is_empty(), "{error:?}"); + assert!(error.failures.is_empty(), "{error:?}"); assert_eq!( error.to_string(), "the root has switched, so its mounts stay" diff --git a/nvx-rs/src/guest/root/tests/view.rs b/nvx-rs/src/guest/root/tests/view.rs index ad8d14411..eeed24152 100644 --- a/nvx-rs/src/guest/root/tests/view.rs +++ b/nvx-rs/src/guest/root/tests/view.rs @@ -95,38 +95,6 @@ fn the_root_removes_its_view_binds_in_reverse_order_before_the_overlay() { ); } -#[cfg(target_arch = "x86_64")] -#[test] -fn an_applied_root_keeps_its_view_binds_when_transferred_into_parts() { - for switched in [false, true] { - let (guest, mut root, machine_id, resolver) = prepared(); - root.begin_view().unwrap(); - root.bind_view(&machine_id, Some(&resolver)).unwrap(); - if switched { - root.switch().unwrap(); - } - let calls = guest.kernel.calls(); - let applied = crate::guest::activation::Applied::from_test_root(root, machine_id.clone()); - let (root, returned_id) = applied.into_parts(); - assert_eq!(returned_id, machine_id); - assert_eq!(guest.kernel.calls(), calls); - assert_eq!(root.view_binds.as_ref().unwrap().1.len(), 2); - if switched { - assert!(root.teardown().unwrap_err().is_switched()); - assert!(guest.kernel.unmounts().is_empty()); - } else { - root.teardown().unwrap(); - assert_eq!( - &guest.kernel.unmounts()[..2], - [ - guest.text("run/nvx/rootfs/etc/resolv.conf"), - guest.text("run/nvx/rootfs/etc/machine-id"), - ] - ); - } - } -} - #[test] fn image_mode_without_dns_binds_nothing_and_a_second_view_is_refused() { let (guest, mut root, _, _) = prepared(); @@ -175,7 +143,7 @@ fn a_later_bind_failure_stays_owned_and_its_teardown_removes_the_earlier_bind() .kind(), ErrorKind::Io ); - assert!(root.is_latched()); + assert!(root.latched); let machine = root.path().join("etc/machine-id"); assert!(guest.kernel.mount_at(&machine).is_some()); assert!(guest.kernel.mount_at(&target).is_none()); @@ -227,7 +195,7 @@ fn actual_resolver_failures_keep_the_network_class_and_root_owned_bind() { .downcast_ref::() .unwrap(); assert_eq!(cause.raw_os_error(), Errno::EPERM as i32); - assert!(root.is_latched()); + assert!(root.latched); } error => panic!("a real resolver failure lost its network class: {error}"), } @@ -277,7 +245,7 @@ fn switched_or_failed_roots_refuse_a_view_before_opening_its_root() { Errno::ENODEV, ); root.mount_export("microvm", Access::ReadOnly).unwrap_err(); - assert!(root.is_latched()); + assert!(root.latched); } let calls = guest.kernel.calls(); assert_eq!( @@ -300,15 +268,16 @@ fn teardown_reports_each_view_bind_that_stays_mounted() { .fail(Operation::Unmount, root.path().join(target), Errno::EBUSY); } let error = root.teardown().unwrap_err(); - let paths: Vec<_> = error - .failures() + let mounts: Vec = error + .failures .iter() - .map(|failure| failure.path().to_path_buf()) + .map(|failure| failure.mount.clone()) .collect(); assert_eq!( - paths, - ["etc/resolv.conf", "etc/machine-id"] - .map(|target| guest.path("run/nvx/rootfs").join(target)) + mounts, + [RESOLVER_TARGET, MACHINE_ID_TARGET].map(|target| Mount::Mapping { + target: target.into() + }) ); } @@ -339,10 +308,13 @@ fn a_view_bind_that_moves_and_cannot_detach_stays_owned_until_teardown() { let torn = root.teardown(); if persistent { let error = torn.unwrap_err(); + let mount = Mount::Mapping { + target: RESOLVER_TARGET.into(), + }; assert!( - error.failures().iter().any(|failure| { - failure.path() == guest.path("run/nvx/rootfs/etc/resolv.conf") - && failure.error().raw_os_error() == Some(Errno::EBUSY as i32) + error.failures.iter().any(|failure| { + failure.mount == mount + && failure.error.raw_os_error() == Some(Errno::EBUSY as i32) }), "{error:?}" ); diff --git a/nvx-rs/src/guest/view.rs b/nvx-rs/src/guest/view.rs index 6c23e8702..0caa42fa4 100644 --- a/nvx-rs/src/guest/view.rs +++ b/nvx-rs/src/guest/view.rs @@ -16,10 +16,6 @@ use crate::activation::{self, MachineIdMode, View}; use crate::protocol::cmdline::write_resolver; use crate::{Error, Result}; -/// The hostname used unless a caller names another. -pub const DEFAULT_HOSTNAME: &str = "nvx-sandbox"; -/// The longest guest hostname, in bytes: Linux's `HOST_NAME_MAX`. -pub const MAX_HOSTNAME_BYTES: usize = 64; /// The generated machine ID's file in the root's runtime directory. pub const MACHINE_ID_FILE: &str = "workload-machine-id"; /// The resolver file in the root's runtime directory.