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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ changes only when the module's purpose does.
| [`nvx::Hypervisor`](#nvxhypervisor) | KVM, MSHV, or WHP, with its serde form behind the `serde` feature |
| [`nvx::exec`](#nvxexec) | The execution engine: the streams, credit, and outcome of an execution's two sides, without I/O, for any framing |
| [`nvx::host::runtime`](#nvxhostruntime) | `Runtime`: what every launch shares, the runtime's files and their identity, the hypervisor and its probe, and the launch options |
| [`nvx::host::vm`](#nvxhostvm) | A private VM directory and its fixed outputs, and an inspectable cold launch with its opened log and control credential |
| [`nvx::host::vm`](#nvxhostvm) | A private VM directory, an inspectable cold preparation, and an owned, verified detached launch |
| [`nvx::host::snapshot`](#nvxhostsnapshot) | Snapshot compatibility keys, strict manifests, restore-time eligibility, and the opaque input of a future VM restore |
| [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity |
| [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener |
Expand Down Expand Up @@ -156,6 +156,7 @@ changes only when the module's purpose does.
| [`nvx::guest::clone`](#nvxguestclone) | x86_64: the preparation of a template's guest for a capture, and the repair and activation of each clone |
| [`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 bootstrap, assembly, shares, export, mappings, 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 |

GPT lower layers use `noload,errors=remount-ro`: the kernel neither loads nor
Expand Down Expand Up @@ -504,13 +505,42 @@ these names before creating any output, including the log.
before a spawn. `log()` returns the same opened log, without reopening its
path. `inherit(file)` moves an already opened resource into the preparation's
inheritance allowlist and keeps it open. `arguments()` returns the whole
configuration's OpenVMM arguments. Creation checks the complete Windows
configuration's OpenVMM arguments. Creation makes caller-relative runtime
and block-image paths absolute without resolving links or `..` components,
so spawning in the VM directory does not change which inputs they name.
Creation checks the complete Windows
command line with the actual runtime and VM output paths and no reserve,
including quoting and the terminating NUL, on every host. Callers keep the
directory and runtime inputs stable through creation and a later spawn.
Dropping a successful preparation closes its resources but leaves its output
paths owned by the caller.

`vm::spawn(prepared)` consumes that preparation and returns `Launched` with
its verified `process`, the same `outputs`, and the same `capability`. The log
and inherited resources are passed as already opened objects, never reopened
by name. The capability reaches OpenVMM only on standard input, then
end-of-file, not in arguments, environment variables, or a file. A failure
after child creation terminates that child and confirms its exit; a cleanup
failure is reported with the original error. Spawn removes no output path.
Dropping `Launched` closes the process handle but does not stop the VM.

On Linux the detached child gets the caller's umask with the owner's bits
cleared; the caller's own umask is unchanged. On Windows it starts without a
console in its log-named job, refusing a taken or untrusted job name, and
leaves the caller's job when the runtime requests breakaway. The inheritance
lock is internal to spawn. Windows callers coordinate their own other spawns,
for example with their own lock around VM and other process starts: an
unrelated process started during the inheritance window may inherit handles,
but never the VM's job claim.

The [VM tests](src/host/vm/tests.rs) cover cleanup after child creation,
ownership, caller survival, the Linux umask, and Windows job claims. Their
ignored cold-spawn smoke test runs the current library against explicit
`NVX_VM_SPAWN_OPENVMM`, `NVX_VM_SPAWN_KERNEL`, and `NVX_VM_SPAWN_INITRAMFS`
paths, using a fresh `NVX_VM_SPAWN_PROOF_DIR` and `NVX_HYPERVISOR` or the
platform's default hypervisor. It checks a guest boot marker,
responsive shell input, and a verified clean exit; it downloads no artifact.

### `nvx::host::snapshot`

`SnapshotKey` and `SnapshotManifest` own the compatibility key and strict
Expand Down Expand Up @@ -1905,7 +1935,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 shares and mappings in reverse order, a runtime mount whose
`Root::teardown`: the view's binds, then the shares and mappings 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
Expand All @@ -1919,6 +1949,25 @@ operations collect no child's status, change the caller's root only by the
switch and never its credentials, start no async runtime, and never power the VM
off.

### `nvx::guest::view`

Hidden, unstable internal reference code of the guest's space, like `root`.
`apply(&mut Root, &activation::View, dns)` sets the current UTS namespace's
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.

The root owns every bind, including an attachment left by a failed call.
Teardown before the switch removes them; after the switch teardown refuses
and drop unmounts nothing. A view is attempted once per root, before its
switch. Refusals return the crate's `Error` with kind `Unsupported`, and
system failures with kind `Io`; an error does not restore the old hostname
or remove generated files. The activation record's hostname limit and
machine-ID modes are unchanged.

### `nvx::guest::gpt`

Hidden, like `nvx::guest::root`, which reads its disks through it: NVX's
Expand Down
16 changes: 16 additions & 0 deletions nvx-rs/public-api/linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,17 @@ method nvx::guest::time::Violation::status
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)
variant nvx::guest::view::MachineId::Image (hidden)
method nvx::guest::view::MachineId::generate (hidden)
const nvx::guest::view::RESOLVER_FILE (hidden)
fn nvx::guest::view::apply (hidden)
fn nvx::guest_path
mod nvx::host
mod nvx::host::artifacts
Expand Down Expand Up @@ -3353,6 +3364,10 @@ fn nvx::host::template::status_evidence
mod nvx::host::vm
struct nvx::host::vm::Capability
method nvx::host::vm::Capability::as_bytes
struct nvx::host::vm::Launched
field nvx::host::vm::Launched::capability
field nvx::host::vm::Launched::outputs
field nvx::host::vm::Launched::process
struct nvx::host::vm::Prepared
method nvx::host::vm::Prepared::arguments
method nvx::host::vm::Prepared::capability
Expand All @@ -3369,6 +3384,7 @@ field nvx::host::vm::VmOutputs::log
field nvx::host::vm::VmOutputs::outcome
fn nvx::host::vm::create
fn nvx::host::vm::create_private_dir
fn nvx::host::vm::spawn
mod nvx::net
struct nvx::net::Cidr
method nvx::net::Cidr::contains
Expand Down
5 changes: 5 additions & 0 deletions nvx-rs/public-api/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2275,6 +2275,10 @@ fn nvx::host::template::status_evidence
mod nvx::host::vm
struct nvx::host::vm::Capability
method nvx::host::vm::Capability::as_bytes
struct nvx::host::vm::Launched
field nvx::host::vm::Launched::capability
field nvx::host::vm::Launched::outputs
field nvx::host::vm::Launched::process
struct nvx::host::vm::Prepared
method nvx::host::vm::Prepared::arguments
method nvx::host::vm::Prepared::capability
Expand All @@ -2291,6 +2295,7 @@ field nvx::host::vm::VmOutputs::log
field nvx::host::vm::VmOutputs::outcome
fn nvx::host::vm::create
fn nvx::host::vm::create_private_dir
fn nvx::host::vm::spawn
mod nvx::net
struct nvx::net::Cidr
method nvx::net::Cidr::contains
Expand Down
37 changes: 4 additions & 33 deletions nvx-rs/src/guest/clone/activation/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
mod tests;

use std::fmt;
use std::fs;
use std::io;
use std::net::{IpAddr, Ipv4Addr};
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

Expand All @@ -23,24 +21,15 @@ use super::super::Devices;
use super::Activation;
use crate::activation::{self, ActivationRecord, Configuration, MachineIdMode, Publication};
use crate::gpt::Expected;
use crate::guest::entry::{self, Entry, Identity, MachineId, RuntimeBinds};
use crate::guest::entry::{self, Entry, Identity, RuntimeBinds};
use crate::guest::ingress::{Announcer, StartError};
use crate::guest::mappings::{self, Access, SourceRoot};
use crate::guest::root::{self, Assembly, AssemblyError, Device, Root, TeardownError};
use crate::guest::view::{MachineId, write_resolver_file};
#[cfg(doc)]
use crate::protocol::cmdline::write_resolver;

/// The name of the file, in the template's runtime directory, to which the activation's
/// application writes the machine ID that it generates for a record that asks for one, as NVX's
/// managed agent writes its own to `/run/nvx/workload-machine-id`.
pub const MACHINE_ID_FILE: &str = "workload-machine-id";

/// The name of the file, in the template's runtime directory, to which the activation's
/// application writes the resolver file that names the DNS server of a record's network, which
/// every execution sees read-only at `/etc/resolv.conf`.
pub const RESOLVER_FILE: &str = "resolv.conf";

/// The mode of the resolver file, whatever the umask: every execution may read it.
const RESOLVER_MODE: u32 = 0o444;
pub use crate::guest::view::{MACHINE_ID_FILE, RESOLVER_FILE};

/// The inputs of a clone's activation that its guest takes from its own configuration, which its
/// template captured, so that every clone of the template has the same: the runtime directory,
Expand Down Expand Up @@ -919,24 +908,6 @@ fn teardown_with<S: System>(
Err(AppliedTeardownError { binds, root })
}

/// Writes a resolver file that names `server` as the guest's one DNS server, as [`write_resolver`]
/// writes one, to a new file at `path`, with mode 0444 whatever the process's umask, so that
/// every execution may read it.
///
/// A file, or a symbolic link, at `path` already fails the call with `AlreadyExists`, and is left
/// alone, so each activation writes its resolver file once.
fn write_resolver_file(path: &Path, server: IpAddr) -> io::Result<()> {
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(RESOLVER_MODE)
.open(path)?;
// The umask narrows the mode that creates the file, which could keep executions of other
// users from reading it.
file.set_permissions(fs::Permissions::from_mode(RESOLVER_MODE))?;
write_resolver(&mut file, server)
}

/// Returns the assembly of the root that `configuration` sets, with `template`'s runtime
/// directory, devices, and timeout.
fn assembly(template: &RootTemplate, configuration: &Configuration) -> Assembly {
Expand Down
37 changes: 0 additions & 37 deletions nvx-rs/src/guest/clone/activation/apply/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use std::error::Error as _;
use std::fs;
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::os::unix::fs::{PermissionsExt, symlink};
use std::rc::Rc;
use std::time::Instant;

Expand Down Expand Up @@ -1420,42 +1419,6 @@ fn a_network_s_dns_server_is_written_to_a_resolver_file_that_every_execution_see
}
}

#[test]
fn a_resolver_file_names_its_dns_server_once_and_every_execution_may_read_it() {
// The file names the server in its canonical text, as NVX's init writes the initramfs's,
// with mode 0444, whatever the umask.
let directory = tempfile::tempdir().unwrap();
let ipv6 = IpAddr::V6(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0xa00, 1));
let cases = [
("v4", DNS, &b"nameserver 10.0.0.1\n"[..]),
("v6", ipv6, &b"nameserver fd00::a00:1\n"[..]),
];
for (name, server, text) in cases {
let path = directory.path().join(name);
sys::tests::with_thread_umask(0o077, || write_resolver_file(&path, server)).unwrap();
assert_eq!(fs::read(&path).unwrap(), text);
let mode = fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o7777, 0o444, "{mode:o}");
}

// A file there already fails the write, and is left alone, and so is the file that a
// symbolic link there names, or the path where a dangling one leads.
let written = directory.path().join("v4");
let error = write_resolver_file(&written, ipv6).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
let link = directory.path().join("link");
symlink(&written, &link).unwrap();
let error = write_resolver_file(&link, ipv6).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(fs::read(&written).unwrap(), b"nameserver 10.0.0.1\n");
let absent = directory.path().join("absent");
let dangling = directory.path().join("dangling");
symlink(&absent, &dangling).unwrap();
let error = write_resolver_file(&dangling, DNS).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
assert!(!absent.exists());
}

#[test]
fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_down() {
// The export's mount fails with the export's class, and the resolver file's write and bind,
Expand Down
Loading
Loading