Skip to content

Commit 03b7ccb

Browse files
ppennaCopilot
andcommitted
host: keep the export's pins in the CLI and paths only in ExportPlan
ExportPlan, the public description of a VM's export, combined the host paths with the file identities that only the CLI's store needs: each child's denied_identities, each bind's identity, and FileIdentity, with check's rules that a recorded plan pins each object that a workload could move. The paths are what nvx lowers; the identities are the CLI's recheck of its own records before each start. The CLI now owns PlannedExport { description: ExportPlan, pins: Pins }, crate-private and cli-gated beside the planner and the verifier that SL6.5a2 made private: - The planner returns a PlannedExport: the description, and the pins of each child's denied paths and of each bind, in their orders, which it computes as before. The binds keep their pins as they are sorted. - PlannedExport::check runs ExportPlan::check, then the pin rules that moved out of it: a slot for each child and each of its denied paths, one for each bind, and the identity of each denied or mapped object strictly inside a part that the guest may write, with the same messages. PlannedExport::verify is the old ExportPlan::verify, which reads the identities from the pins. - The sandbox's config.json keeps the description under export, paths only, where CI's share checks read it, and records the pins beside it under export_pins, as it records each image beside its binding. Config::new requires the field and refuses an export without pins or pins without an export as malformed; Config::check checks and verifies the pair before vm_config passes the description to the VM. plan_mounts, provision's ProvisionConfig and check_images carry the PlannedExport; nvx-rs run, which launches its plan at once and never again, keeps the description and checks no pins, as before. nvx's public API loses FileIdentity, ExportChild::denied_identities, Bind::identity, and ExportPlan::openvmm_arguments and kernel_token, which only VmConfig's lowering calls and which become pub(crate); lowering is unchanged. ExportPlan::check stays structural and needs no identity data. host::filesystem documents what the caller keeps stable: the launch race, the writable-part recheck and durable denials. A configuration that an earlier build wrote has no export_pins, so a start refuses it as "sandbox configuration has no export pins; deprovision the sandbox and provision it again", as for every field that the record gained before; nothing is migrated, and the format stays 10. Tests: the planner's and verifier's unit tests read the pins from the PlannedExport, and narrowed_writes_pin_only_what_lies_in_writable_paths also checks the new slot rules; ExportPlan's own tests and fixtures, in host::config, host::vm, the restore tests and tests/cli/openvmm.rs, build plans without identities. A new state test records an export with its pins, reads both back, and refuses a moved, a gone and an uninspectable pinned object at Config::check. The CLI's lifecycle tests expect export_pins in each record, and malformed_configuration refuses a record without it, an export without pins, and pins without an export. Part of #1125 (SL6.5b). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 19b5404 commit 03b7ccb

25 files changed

Lines changed: 620 additions & 355 deletions

File tree

‎doc/acceptance-case-matrix.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ The adapter's README said what each proved.
242242
| `a_run_whose_clone_does_not_start_runs_nothing` | retired | Template selections and the template's `run` leave with the old clone path (#1121) |
243243
| `a_run_whose_clone_s_vm_exits_during_its_execution_ends_with_the_clone_s_failure` | retired | The template's `run`, clone records and leases leave with the old clone path (#1121) |
244244
| `a_selection_that_names_no_template_leases_nothing` | retired | Template selections and leases leave with the old clone path (#1121) |
245-
| `a_start_refuses_an_export_whose_mapped_objects_changed_before_any_vmm_starts` | rewritten | `nvx-rs/src/host/filesystem/tests.rs::an_exported_object_that_a_workload_could_move_is_checked_again_before_a_start`; `nvx-rs/src/host/filesystem/tests.rs::swapped_objects_fail_verification` |
245+
| `a_start_refuses_an_export_whose_mapped_objects_changed_before_any_vmm_starts` | rewritten | `nvx-rs/src/host/filesystem/tests.rs::an_exported_object_that_a_workload_could_move_is_checked_again_before_a_start`; `nvx-rs/src/host/filesystem/tests.rs::swapped_objects_fail_verification`; `nvx-rs/src/host/cli/sandbox/state/tests.rs::records_keep_the_export_with_its_pins_and_starts_refuse_changed_pins`, whose record keeps the pins beside the export, and whose start refuses a changed pin before the VM's configuration takes the export (SL6.5b) |
246246
| `a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts` | rewritten | `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_refuses_an_image_longer_than_its_placeholder`; `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_refuses_each_scratch_geometry_mismatch`; `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_refuses_each_memory_mismatch_without_rounding_or_overflow` |
247247
| `a_template_without_a_nic_serves_clones_without_a_network` | rewritten | `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_refuses_both_nic_presence_mismatches_and_each_named_address_mismatch` |
248248
| `a_tombstone_leaves_the_running_clone_of_its_generation_to_its_lease` | retired | Tombstones, generations and leases leave with the old clone path (#1121) |

‎doc/run.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,13 +1300,15 @@ inside a writable part of a share. On Windows, it refuses a path that only a
13001300
verbatim `\\?\` path names, such as that of a directory whose name ends in a
13011301
period or a space, which the path without the prefix would not name. The
13021302
shares' children follow their resolved host paths. A managed sandbox stores
1303-
the plan in its typed configuration, in format 10. Each `start` checks it again,
1304-
as `run` does once it has verified the distro layer: it refuses a host path
1305-
that no longer holds a directory or regular file as recorded, a recorded path
1306-
whose file identity changed, and shares whose host paths came to overlap, such
1307-
as through a hard link or a bind mount. NVX no longer reads earlier
1308-
configuration formats, including those that recorded the shares as requested;
1309-
deprovision such a sandbox and provision it again.
1303+
the plan, with those identities beside it, in its typed configuration, in
1304+
format 10. Each `start` checks it again, as `run` does once it has verified the
1305+
distro layer: it refuses a host path that no longer holds a directory or
1306+
regular file as recorded, a recorded path whose file identity changed, and
1307+
shares whose host paths came to overlap, such as through a hard link or a bind
1308+
mount. NVX no longer reads earlier configuration formats, including those that
1309+
recorded the shares as requested, nor a configuration that earlier builds wrote
1310+
without those identities beside the plan; deprovision such a sandbox and
1311+
provision it again.
13101312

13111313
A share must not reach the sandbox's own files, which the workload could
13121314
otherwise read or change: `provision` refuses a share whose resolved host path

‎nvx-rs/README.md‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -937,16 +937,26 @@ action that needs more than 256 rules.
937937

938938
The `ExportPlan` of host paths through OpenVMM's aggregate virtio-fs device, for
939939
a root's `ReservedTargets`, the blockless root's or the container root's: its
940-
children, its ordered binds, which give the host-mapping table, and the
941-
identities that each start checks again; `ExportPlan::check`, which checks a
942-
recorded plan without reading the host, and the plan's OpenVMM options, boot
943-
token, and mapping table; and `guest_path` (also at `nvx::`), the guest path at
944-
which the host-path convention places a resolved host path.
940+
children and its ordered binds, which give the host-mapping table, paths only;
941+
`ExportPlan::check`, which checks a recorded plan without reading the host, and
942+
the plan's mapping table, `ExportPlan::guest_table`; and `guest_path` (also at
943+
`nvx::`), the guest path at which the host-path convention places a resolved
944+
host path. A `VmConfig` lowers the plan into OpenVMM's options and its
945+
`nvx_maps` boot token internally.
946+
947+
OpenVMM resolves the plan's paths only when it starts, so the caller keeps them
948+
naming the objects that it means, as the module documents: across the launch
949+
race, from its own checks of the host until OpenVMM opens the paths; through a
950+
writable-part recheck before each later launch, since a workload that may write
951+
a part of a child can move a mapped or denied object there and put a decoy at
952+
its path; and for each durable denial, which holds only while its path names
953+
the object that it hides.
945954

946955
The planner, which turns the mapped host paths at their guest targets, their
947-
denied, allowed, and write paths, and the owner into a plan, and the checks of a
956+
denied, allowed, and write paths, and the owner into a plan, with the pins of
957+
the objects that a workload could move, and the checks of those pins and of the
948958
plan's host objects before each start are the CLI's path rules, private to the
949-
crate (feature `cli`).
959+
crate (feature `cli`). The CLI records the pins beside the plan.
950960

951961
### `nvx::host::nvxs`
952962

@@ -1930,7 +1940,7 @@ provision it again:
19301940

19311941
| File | Contents |
19321942
| --- | --- |
1933-
| `config.json` | The provisioned sandbox, in format 10 only: `image` holds its format, path and identity; `upper` is tagged as scratch with its path or RAM with `max_memory_percent`. `distro_binding` binds the image, and `scratch_binding` binds scratch or is `null` for RAM. The export is typed, `null` when absent. `network`, `null` without `--net`, holds `--net`, `--network-egress`, `--network-ingress`, `--host-loopback`, `--network-proxy`, each `null` when not given, and the `--host-loopback-forward` options, as given, with the egress rules in OpenVMM's form, from a policy file's compile if one was given. A sandbox that `snapshot restore` provisioned also records its snapshot's name under `source_snapshot`, the absolute path of the snapshot's store under `source_store`, and the program and arguments of the workload that each restore runs under `workload`; one that boots cold records none of them. An atomic pretty-JSON write uses mode 0600, and start reads the same strict typed fields without a legacy reader. |
1943+
| `config.json` | The provisioned sandbox, in format 10 only: `image` holds its format, path and identity; `upper` is tagged as scratch with its path or RAM with `max_memory_percent`. `distro_binding` binds the image, and `scratch_binding` binds scratch or is `null` for RAM. The export is typed, `null` when absent, and holds paths only; `export_pins`, `null` without an export, holds the identities of the export's denied and mapped objects that a workload could move, which each start checks before it launches the export, and a configuration without it is refused with the advice to deprovision the sandbox and provision it again. `network`, `null` without `--net`, holds `--net`, `--network-egress`, `--network-ingress`, `--host-loopback`, `--network-proxy`, each `null` when not given, and the `--host-loopback-forward` options, as given, with the egress rules in OpenVMM's form, from a policy file's compile if one was given. A sandbox that `snapshot restore` provisioned also records its snapshot's name under `source_snapshot`, the absolute path of the snapshot's store under `source_store`, and the program and arguments of the workload that each restore runs under `workload`; one that boots cold records none of them. An atomic pretty-JSON write uses mode 0600, and start reads the same strict typed fields without a legacy reader. |
19341944
| `sandbox.lock` | The store's lock, which serializes the lifecycle transitions |
19351945
| `exec.lock` | The exec lock, which an `exec` holds from before it finds the running VM until its workload ends, so that one `exec` at a time runs in the sandbox; it stays from the first `exec` until `deprovision` |
19361946
| `launch.json` | The store's journal of a start in progress, or of one that was interrupted |

‎nvx-rs/public-api/linux.txt‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -943,14 +943,12 @@ mod nvx::host::filesystem
943943
struct nvx::host::filesystem::Bind
944944
field nvx::host::filesystem::Bind::child
945945
method nvx::host::filesystem::Bind::guest_source
946-
field nvx::host::filesystem::Bind::identity
947946
field nvx::host::filesystem::Bind::read_only
948947
field nvx::host::filesystem::Bind::source
949948
field nvx::host::filesystem::Bind::target
950949
struct nvx::host::filesystem::ExportChild
951950
field nvx::host::filesystem::ExportChild::allowed
952951
field nvx::host::filesystem::ExportChild::denied
953-
field nvx::host::filesystem::ExportChild::denied_identities
954952
field nvx::host::filesystem::ExportChild::file
955953
field nvx::host::filesystem::ExportChild::root
956954
field nvx::host::filesystem::ExportChild::writable
@@ -960,12 +958,7 @@ field nvx::host::filesystem::ExportPlan::binds
960958
method nvx::host::filesystem::ExportPlan::check
961959
field nvx::host::filesystem::ExportPlan::children
962960
method nvx::host::filesystem::ExportPlan::guest_table
963-
method nvx::host::filesystem::ExportPlan::kernel_token
964-
method nvx::host::filesystem::ExportPlan::openvmm_arguments
965961
field nvx::host::filesystem::ExportPlan::owner
966-
struct nvx::host::filesystem::FileIdentity
967-
field nvx::host::filesystem::FileIdentity::device
968-
field nvx::host::filesystem::FileIdentity::index
969962
const nvx::host::filesystem::GUEST_EXPORT
970963
const nvx::host::filesystem::MAX_CHILDREN
971964
enum nvx::host::filesystem::Owner

‎nvx-rs/public-api/windows.txt‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,12 @@ mod nvx::host::filesystem
401401
struct nvx::host::filesystem::Bind
402402
field nvx::host::filesystem::Bind::child
403403
method nvx::host::filesystem::Bind::guest_source
404-
field nvx::host::filesystem::Bind::identity
405404
field nvx::host::filesystem::Bind::read_only
406405
field nvx::host::filesystem::Bind::source
407406
field nvx::host::filesystem::Bind::target
408407
struct nvx::host::filesystem::ExportChild
409408
field nvx::host::filesystem::ExportChild::allowed
410409
field nvx::host::filesystem::ExportChild::denied
411-
field nvx::host::filesystem::ExportChild::denied_identities
412410
field nvx::host::filesystem::ExportChild::file
413411
field nvx::host::filesystem::ExportChild::root
414412
field nvx::host::filesystem::ExportChild::writable
@@ -418,12 +416,7 @@ field nvx::host::filesystem::ExportPlan::binds
418416
method nvx::host::filesystem::ExportPlan::check
419417
field nvx::host::filesystem::ExportPlan::children
420418
method nvx::host::filesystem::ExportPlan::guest_table
421-
method nvx::host::filesystem::ExportPlan::kernel_token
422-
method nvx::host::filesystem::ExportPlan::openvmm_arguments
423419
field nvx::host::filesystem::ExportPlan::owner
424-
struct nvx::host::filesystem::FileIdentity
425-
field nvx::host::filesystem::FileIdentity::device
426-
field nvx::host::filesystem::FileIdentity::index
427420
const nvx::host::filesystem::GUEST_EXPORT
428421
const nvx::host::filesystem::MAX_CHILDREN
429422
enum nvx::host::filesystem::Owner

‎nvx-rs/src/host/cli/run.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ fn cold_config(options: &Options, rules: &NativeRules) -> Result<VmConfig> {
616616
read_only: *access == MountAccess::ReadOnly,
617617
})
618618
.collect();
619+
// A run launches its export right after it plans it, and never again, so it checks no pins.
619620
let filesystem = filesystem::plan(&ExportRequest {
620621
mappings: &mappings,
621622
denied: PathList {
@@ -637,7 +638,8 @@ fn cold_config(options: &Options, rules: &NativeRules) -> Result<VmConfig> {
637638
},
638639
reserved: ReservedTargets::BLOCKLESS_ROOT,
639640
})
640-
.map_err(error_with_cause)?;
641+
.map_err(error_with_cause)?
642+
.map(|planned| planned.description);
641643
cold_vm_config(options, rules, filesystem)
642644
}
643645

‎nvx-rs/src/host/cli/run/guest_tokens/tests.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ fn planned_boot_shares_keep_table_sources_targets_access_and_file_kind() {
7474
reserved: ReservedTargets::BLOCKLESS_ROOT,
7575
})
7676
.unwrap()
77-
.unwrap();
77+
.unwrap()
78+
.description;
7879
let table = plan.guest_table();
7980
assert_eq!(table[1].source, "0/single.txt");
8081
let line = render("", Some(&plan)).unwrap();

‎nvx-rs/src/host/cli/sandbox/lifecycle/mod.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! Persistent sandboxes on the CLI's store and caller-owned [`crate::host::vm`] interface.
22
//!
3-
//! Provision records the typed image and upper, image bindings, network and export. Start journals the
4-
//! prepared capability and outputs before spawn, records the verified process, and commits the
5-
//! matching saved VM only after authenticated readiness and mappings. Every lifecycle command
6-
//! takes the CLI store's recoverable lock; indeterminate launches retain their evidence.
3+
//! Provision records the typed image and upper, image bindings, network, and export with its pins.
4+
//! Start journals the prepared capability and outputs before spawn, records the verified process,
5+
//! and commits the matching saved VM only after authenticated readiness and mappings. Every
6+
//! lifecycle command takes the CLI store's recoverable lock; indeterminate launches retain their
7+
//! evidence.
78
//!
89
//! A restored sandbox, whose configuration names a named snapshot, restores it at each start in
910
//! place of a cold boot, under the snapshot's lock, through the blocking restore driver, which
@@ -38,7 +39,7 @@ use crate::host::cli::sandbox::{Artifacts, error_with_cause};
3839
use crate::host::cli::session::Session;
3940
use crate::host::cli::store::StateFile;
4041
use crate::host::cli::{Exit, Result};
41-
use crate::host::filesystem::ExportPlan;
42+
use crate::host::filesystem::PlannedExport;
4243
use crate::host::process::{Opened, Process};
4344
use crate::host::runtime::{IdentityPart, LaunchOptions, Runtime, RuntimeIdentity};
4445
use crate::host::sandbox::layer::SandboxLayer;
@@ -74,8 +75,8 @@ pub(crate) struct ProvisionConfig<'a> {
7475
pub(crate) hostname: &'a str,
7576
/// `--workload-user`.
7677
pub(crate) workload_identity: (u32, u32),
77-
/// The export plan of the shares, if any.
78-
pub(crate) export: Option<&'a ExportPlan>,
78+
/// The export plan of the shares, with its pins, if any.
79+
pub(crate) export: Option<&'a PlannedExport>,
7980
/// The network options, or `None` without `--net`.
8081
pub(crate) network: Option<&'a NetworkRecord>,
8182
}

‎nvx-rs/src/host/cli/sandbox/lifecycle/start/tests.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fs;
44
use std::path::PathBuf;
55

66
use super::*;
7-
use crate::host::filesystem::ExportPlan;
7+
use crate::host::filesystem::PlannedExport;
88
use crate::host::paths::resolve;
99
use crate::host::sandbox::distro::DistroBinding;
1010
use crate::host::sandbox::layer::{LayerRole, SandboxLayer};
@@ -111,11 +111,12 @@ fn starts_check_the_kernel_command_line_that_they_launch() {
111111
let export = |shares: &[String]| plan_mounts(&sandbox_mounts(shares, &[], None).unwrap());
112112
let read_only = export(std::slice::from_ref(&settings)).unwrap();
113113
let writable = export(&[settings, output]).unwrap();
114+
let tokens = |export: &PlannedExport| export_tokens(&export.description);
114115
assert_eq!(
115-
export_tokens(read_only.as_ref().unwrap()),
116+
tokens(read_only.as_ref().unwrap()),
116117
" virtfs_dir=/run/nvx/hostfs/root virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1"
117118
);
118-
let configure = |cmdline: &str, scratch: Option<&Path>, export: Option<&ExportPlan>| {
119+
let configure = |cmdline: &str, scratch: Option<&Path>, export: Option<&PlannedExport>| {
119120
let config = ProvisionConfig {
120121
hypervisor: "kvm",
121122
memory_mib: 256,
@@ -130,7 +131,7 @@ fn starts_check_the_kernel_command_line_that_they_launch() {
130131
let vm = config.vm_config().unwrap();
131132
(config, vm)
132133
};
133-
let refusal = |cmdline: &str, scratch: Option<&Path>, export: Option<&ExportPlan>| {
134+
let refusal = |cmdline: &str, scratch: Option<&Path>, export: Option<&PlannedExport>| {
134135
let (config, vm) = configure(cmdline, scratch, export);
135136
let error = check_command_line(&config, &vm).unwrap_err();
136137
assert_eq!(error.code(), Some(ErrorCode::Unsupported), "{error:?}");
@@ -139,7 +140,7 @@ fn starts_check_the_kernel_command_line_that_they_launch() {
139140
for upper in [None, Some(scratch.as_path())] {
140141
for export in [None, read_only.as_ref(), writable.as_ref()] {
141142
let (_, vm) = configure("", upper, export);
142-
let fragment = export.map(export_tokens).unwrap_or_default();
143+
let fragment = export.map(tokens).unwrap_or_default();
143144
let room =
144145
COMMAND_LINE_BUDGET - vm.kernel_command_line().unwrap().len() - fragment.len() - 2;
145146
let (config, vm) = configure(&"x".repeat(room), upper, export);

‎nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,6 @@ fn restored_config(fixture: &Fixture, gpt: bool, ram: bool) -> VmConfig {
268268
file: false,
269269
writable: false,
270270
denied: Vec::new(),
271-
denied_identities: Vec::new(),
272271
allowed: Vec::new(),
273272
write: Vec::new(),
274273
}],
@@ -277,7 +276,6 @@ fn restored_config(fixture: &Fixture, gpt: bool, ram: bool) -> VmConfig {
277276
source: String::new(),
278277
target: "/work".to_owned(),
279278
read_only: true,
280-
identity: None,
281279
}],
282280
owner: Owner::Vmm,
283281
});

0 commit comments

Comments
 (0)