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
7 changes: 5 additions & 2 deletions .github/actions/check-nvx-rs/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ runs:
-- -D warnings
cargo "+${{ steps.toolchains.outputs.toolchain }}" clippy --locked --lib --no-default-features \
-- -D warnings
# The cli and testing features alone, so that the compiler checks the private code that
# each feature set builds, which other features' code may otherwise use.
# The serde, cli and testing features alone, so that the compiler checks the code that each
# feature set builds, which other features' code may otherwise use. The guest feature
# alone is checked with the guest binaries, below, for the guests' target.
cargo "+${{ steps.toolchains.outputs.toolchain }}" clippy --locked --lib \
--no-default-features --features serde -- -D warnings
cargo "+${{ steps.toolchains.outputs.toolchain }}" clippy --locked --lib --bins \
--features cli -- -D warnings
cargo "+${{ steps.toolchains.outputs.toolchain }}" clippy --locked --lib --bins \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ nvx-rs\target\release\nvx-rs.exe run
- [Module diagrams](doc/modules/README.md) - Diagrams of the modules of the `nvx` crate and the
dependencies between them.
- [Continuous integration](doc/ci.md) - Instructions for running and maintaining NVX CI.
- [Case matrix](doc/acceptance-case-matrix.md) - The fate of each test of the runtime adapter, of
CI and of the docs, and of each microVM scenario, before NVX deletes the adapter.
- [Copilot-driven adversarial testing](doc/adversarial-testing.md) - Bounded adaptive stress and
containment campaigns.
- [Package and source delivery](doc/distribution.md) - Instructions for packaging and distributing
Expand Down
454 changes: 454 additions & 0 deletions doc/acceptance-case-matrix.md

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions doc/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -2311,14 +2311,17 @@ which holds the NVX mechanics, on GitHub-hosted Ubuntu and Windows runners
through the [`check-nvx-rs`](../.github/actions/check-nvx-rs/action.yml)
action, on the Rust release that `rust-toolchain.toml` pins. It runs rustfmt,
Clippy with warnings denied for all targets and features, for the library with
and without default features, and for the `cli` and `testing` features alone,
the unit, protocol
and without default features, and for the `serde`, `cli` and `testing`
features alone, the unit, protocol
conformance-vector and property, and fake-OpenVMM instance tests, a test that
the direct children of `nvx::host` depend on one another without cycles, rustdoc
with warnings denied, the minimum Rust version check, and the macOS cross-check.
Clippy checks `cli` alone and `testing` alone so that the compiler checks the
private code that each of them builds, which the code of other features may
otherwise use.
Clippy checks `serde`, `cli` and `testing` alone so that the compiler checks the
code that each of them builds, which the code of other features may
otherwise use; the musl check of the guest binaries checks `guest` alone.
Clippy's checks of all targets compile the crate's examples, among them the
compile-only `host-workflows` and, in the musl check, `guest-workflows`, so
that the documented workflows keep compiling against the public API.
It also validates bundled artifact staging with a fixture package. The action
sets `NVX_BUNDLE=skip` for `--all-features` builds, so that `bundled` stages
nothing instead of downloading the pinned release.
Expand Down
2 changes: 1 addition & 1 deletion doc/contribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ the Python, shell, and link checks.
| `scripts/setup/setup-windows-whp.ps1` | PowerShell syntax | [check-quality](../.github/actions/check-quality/action.yml) |
| Markdown files and `doc/` paths | Every relative link and image in a tracked Markdown file resolves to a tracked file, and every anchor to a heading slug that GitHub generates, ignoring code. Every `doc/` path that a tracked file mentions resolves too, except the test fixtures that `DOC_PATH_FIXTURES` lists. | [check-quality](../.github/actions/check-quality/action.yml), [check_markdown_links.py](../scripts/check_markdown_links.py) |
| Tests | Every test lives in a test file, and production files hold only the test hooks that the allowlist allows; see [Test layout](#test-layout) | [check-quality](../.github/actions/check-quality/action.yml), [check_test_layout.py](../scripts/check_test_layout.py) |
| `nvx-rs/` | rustfmt, Clippy and rustdoc with warnings denied, Clippy also for the `cli` and `testing` features alone, tests including a bundled-release fixture, the minimum supported Rust version, a macOS check, the public API against its snapshots in `nvx-rs/public-api/`, and, through `nvx.py`, the musl lint, tests, build, digest manifest, and static-ELF check of the guest binaries; see [Rust crates](ci.md#rust-crates) | [check-nvx-rs](../.github/actions/check-nvx-rs/action.yml), [public_api.py](../scripts/public_api.py) |
| `nvx-rs/` | rustfmt, Clippy and rustdoc with warnings denied, Clippy also for the `serde`, `cli` and `testing` features alone, tests including a bundled-release fixture, the minimum supported Rust version, a macOS check, the public API against its snapshots in `nvx-rs/public-api/`, and, through `nvx.py`, the musl lint, tests, build, digest manifest, and static-ELF check of the guest binaries; see [Rust crates](ci.md#rust-crates) | [check-nvx-rs](../.github/actions/check-nvx-rs/action.yml), [public_api.py](../scripts/public_api.py) |
| `tests/runtime-adapter/` | rustfmt, then Clippy and rustdoc with warnings denied, the build, the tests, and the minimum supported Rust version of `nvx`, in the protocol-only, host-only, and, on Linux, static musl guest-only configurations; see [Rust crates](ci.md#rust-crates) | [check-nvx-rs](../.github/actions/check-nvx-rs/action.yml) |
| NVX CLI | Compiles `scripts/`, runs every `scripts/test_*.py` module, and checks command help, on Linux and Windows, and compares the copies of OpenVMM's protocol vectors in `nvx-rs/tests/vectors/openvmm/`, and of the identities of its built-in CPU profiles in `nvx-rs/src/host/cli/cpu_generations.json`, with the submodule's files | [validate-nvx](../.github/actions/validate-nvx/action.yml), [ci.yml](../.github/workflows/ci.yml) |

Expand Down
4 changes: 3 additions & 1 deletion doc/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ nvx/
|-- docker/
| `-- Dockerfile Reproducible guest build environment
|-- doc/ User and contributor documentation
| |-- acceptance-case-matrix.md The fate of each adapter, CI and docs test, and microVM scenario
| |-- adversarial-testing.md Copilot-driven adversarial testing
| |-- benchmarks.md Benchmark commands and measurement methodology
| |-- build.md Guest and OpenVMM build workflows
Expand Down Expand Up @@ -105,7 +106,8 @@ nvx/
| | |-- testing/ Fake OpenVMM (feature `testing`)
| | |-- guest/ Programs that run in Linux guests, the guest's NVXS connection, the libraries through which any guest init or agent opens the terminal on which it serves its session, starts a program, collects its children's statuses, settles its executions' cgroups, drives each execution, mounts host mappings beneath the descriptors of the roots that it names, assembles and tears down a layered root, and reads a GPT disk's partition table through its block device, and the typed calls through which it runs the time ABI's steps and the reseed (feature `guest`)
| | `-- bin/ nvx-rs CLI, nvx-fake-openvmm test double, and the guest binaries
| |-- tests/ Fake-OpenVMM instance lifecycle, sandbox store, CLI scenario, NVXS session, guest-binary, and hypervisor managed-lifecycle tests, the store's record fixtures and sandbox configurations of every format, the cases of the export and root boot tokens, and the conformance vectors of the wire formats
| |-- examples/ The external-launch example, and the documented host and guest workflows, which only compile
| |-- tests/ Fake-OpenVMM instance lifecycle, sandbox store, CLI scenario, NVXS session, guest-binary, and hypervisor managed-lifecycle tests, the security checklist, the store's record fixtures and sandbox configurations of every format, the cases of the export and root boot tokens, and the conformance vectors of the wire formats
| |-- public-api/ The crate's public API on Linux and on Windows, which CI checks
| |-- build.rs Stages the bundled artifacts (feature `bundled`)
| `-- artifacts.json Release package pinned for the bundled artifacts
Expand Down
11 changes: 11 additions & 0 deletions nvx-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ name = "external-launch"
path = "examples/external-launch.rs"
required-features = ["host"]

# Compile-only examples of the documented host and guest workflows, which nothing runs.
[[example]]
name = "host-workflows"
path = "examples/host-workflows.rs"
required-features = ["host"]

[[example]]
name = "guest-workflows"
path = "examples/guest-workflows.rs"
required-features = ["guest"]

# The guest binaries. Their unit tests live in the library, so the binaries have no test harness.
[[bin]]
name = "nvx-console-pending"
Expand Down
32 changes: 30 additions & 2 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3507,11 +3507,39 @@ python3 ../scripts/nvx.py test-guest-binaries \
--binaries ../build/nvx-rs-guest-binaries
```

The [security checklist](tests/security_checklist.rs) checks, through the
public API alone and without a VM, each security requirement that the host's
documentation states: the guest targets of an export and a read-only path
inside a writable one, the checks of a staged image and the claim of a scratch
image, which callers keep in directories that only their own account can write,
the exported objects that each start checks again, a VM's private directory and
its new private log, the exit proof of a saved VM, the owner of its endpoint,
and its recovery from the saved values, and a snapshot's strict directory. An
idle copy of the test binary stands in for a VM's process. The checks that NVX
makes of a restore need a captured snapshot, so the unit tests of
`nvx::host::vm`, `nvx::host::snapshot`, and `nvx::host::restore` check them.

Two examples compile the documented workflows through the public API alone,
and nothing runs them: [`host-workflows`](examples/host-workflows.rs), a cold
boot, a capture into a snapshot, a restore from it, finding a VM again after its
caller restarts, and a stop; and [`guest-workflows`](examples/guest-workflows.rs),
for Linux guests, an init's time daemon, control session, capture, activation
of a restore, and mapping table. The guest example leaves the guest's space to
its caller: the steps of NVX's reference space, as nvx-init takes them, compile
in `guest::init::boot::tests::the_reference_space_steps_compile`. Clippy
compiles the examples with the rest of the crate's targets, and
[`external-launch`](examples/external-launch.rs) runs a cold boot end to end,
or a restore up to its restore-ready event.

The [case matrix](../doc/acceptance-case-matrix.md) gives each test of the
[runtime adapter](../tests/runtime-adapter/README.md), of CI, and of the docs,
and each microVM scenario, a fate: kept, rewritten, or retired.

The
[`check-nvx-rs`](../.github/actions/check-nvx-rs/action.yml) action runs
rustfmt, Clippy with warnings denied for all features, for the library
with and without default features, and for the `cli` and `testing` features
alone, the tests, a build and the tests of the library
with and without default features, and for the `serde`, `cli` and `testing`
features alone, the tests, a build and the tests of the library
without default features, a synthetic-release `bundled` test, rustdoc with
warnings denied, the minimum supported Rust version (1.89), and a macOS check.
On Linux, it also lints, builds, and tests the guest binaries through `nvx.py`,
Expand Down
152 changes: 152 additions & 0 deletions nvx-rs/examples/guest-workflows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! A guest init's documented workflows through the public API alone: its boot's time daemon, its
//! control session, the capture of its VM, the activation of a restore, and the host's mapping
//! table.
//!
//! The example only compiles: CI builds it for Linux guests with the `guest` feature, and nothing
//! calls its functions. `nvx` builds no guest space and runs no execution: each init does both,
//! so the example leaves them to its caller. NVX's own init, nvx-init, builds its space with NVX's
//! internal reference code, whose steps compile in nvx-init's unit tests instead.
//!
//! The example's init speaks its own protocol over the session's DATA, in which one payload
//! carries one message, and it reads its session without a deadline. A real init waits for its
//! session with the activation's deadline, and ticks the activation then, and gives the
//! activation what its session received while it applied a record before it sends Ready, as
//! [`Activation`]'s documentation shows.

#[cfg(target_os = "linux")]
pub use linux::*;

fn main() {}

/// The workflows, which run in Linux guests only.
#[cfg(target_os = "linux")]
mod linux {
use std::error::Error;
use std::io::{self, Read, Write};
use std::time::{Duration, Instant};

use nvx::activation::{ActivationRecord, ReadyActivation};
use nvx::guest::activation::{Activation, ActivationAction, ActivationInput};
use nvx::guest::mappings::{Mapping, MappingTable, TableError};
use nvx::guest::nvxs::{GuestConnection, GuestConnectionError};
use nvx::guest::snapshot::{
self, CaptureFailure, Captured, Devices, Disks, Disposition, Repaired,
};
use nvx::guest::time::{TimeAbi, TimeError};
use nvx::protocol::OUTER_RECEIVE_WINDOW;
use nvx::protocol::nvxs::GuestEvent;

/// Starts the time daemon at boot, before anything that it must not inherit opens, and returns
/// its process ID, which the init supervises: the VM ends when the daemon does.
///
/// The daemon is the one process that `nvx` starts in a guest.
pub fn boot() -> Result<u32, TimeError> {
let abi = TimeAbi::new();
let cold = abi.cold_boot()?;
abi.start_daemon(cold)
}

/// Attaches the guest's side of the control session on `console`, the control console, which
/// the init opened in raw mode.
pub fn attach<S: Read + Write>(console: S) -> Result<GuestConnection<S>, GuestConnectionError> {
let mut connection = GuestConnection::new(console);
connection.attach()?;
Ok(connection)
}

/// What the init does once [`capture`] returned.
pub enum AfterCapture {
/// The guest goes on, without a snapshot, or after a failure that left it running: the
/// init reports either on the same session.
Continue(Option<CaptureFailure>),
/// A restore started this guest, and repaired it: the init activates it with
/// [`activate`].
Activate(Repaired),
/// The guest cannot go on: the init starts nothing more, and ends the VM with this
/// status.
End(u8),
}

/// Captures the VM once the host asked for a snapshot and the init answered the request.
///
/// The init keeps its executions still, and its session sends nothing until `capture`
/// returns.
pub fn capture(devices: &Devices, deadline: Instant) -> AfterCapture {
match snapshot::capture(devices, Disks::Fresh, deadline) {
Ok(Captured::Continued) => AfterCapture::Continue(None),
Ok(Captured::Restored(repaired)) => AfterCapture::Activate(repaired),
Err(failure) => match failure.disposition {
Disposition::Resumed => AfterCapture::Continue(Some(failure)),
Disposition::MustEnd { status } => AfterCapture::End(status),
// A disposition of a later release: the guest cannot tell that it may go on.
_ => AfterCapture::End(nvx::protocol::status::TIME_REPAIR),
},
// An outcome of a later release: the guest cannot tell that it may go on.
Ok(_) => AfterCapture::End(nvx::protocol::status::TIME_REPAIR),
}
}

/// Activates a restored VM on its session, once [`capture`] returned its repair.
///
/// The restore queued a reset of the session that the guest attached before the capture,
/// which the init takes and acknowledges, abandoning the earlier session's work; then the
/// host's Activate brings the restore's own settings. `record` reads the bytes of an Activate's
/// record from one of the init's messages, `space` builds the guest's space from the record,
/// which is the init's own code, as nvx-init's is its own, and `ready` sends Ready in the
/// init's protocol. Every failure is final, and the init ends the VM.
pub fn activate<S: Read + Write>(
connection: &mut GuestConnection<S>,
repaired: &Repaired,
record: impl Fn(&[u8]) -> Option<&[u8]>,
space: impl FnOnce(&ActivationRecord) -> Result<(), Box<dyn Error + Send + Sync>>,
mut ready: impl FnMut(&mut GuestConnection<S>, ReadyActivation) -> io::Result<()>,
) -> Result<Activation, Box<dyn Error>> {
let mut activation = Activation::new(repaired, Duration::from_secs(30), Instant::now());
let mut space = Some(space);
loop {
let action = match connection.next_event()? {
GuestEvent::Reset(_) => activation.input(Instant::now(), ActivationInput::Reset)?,
GuestEvent::Data(payload) => {
connection.credit(u32::try_from(payload.len())?)?;
let input = record(&payload).map_or(ActivationInput::Request, |record| {
ActivationInput::Activate(record)
});
activation.input(Instant::now(), input)?
}
};
match action {
ActivationAction::Acknowledge => connection.acknowledge(OUTER_RECEIVE_WINDOW)?,
ActivationAction::Continue | ActivationAction::Join | ActivationAction::Serve => {}
ActivationAction::Apply(record) => {
let space = space.take().ok_or("the activation applied twice")?;
// The activation completes, or expires past its deadline, when the application
// ends.
let applied = space(&record);
let established = activation.complete(Instant::now(), applied)?;
ready(connection, established)?;
return Ok(activation);
}
ActivationAction::Ready(established) => ready(connection, established)?,
}
}
}

/// Applies one part of the host's mapping table, in order, and returns whether the table is
/// complete, after which the init may start workloads, and not before.
///
/// `mount` binds one mapping beneath the init's root, from the host's export, which is the
/// init's own code. A mapping that fails latches the table, which refuses every later part.
pub fn map(
table: &mut MappingTable,
part: &[u8],
mut mount: impl FnMut(&Mapping) -> io::Result<()>,
) -> Result<bool, TableError<io::Error>> {
table.apply(part, |mappings| {
mappings
.iter()
.enumerate()
.try_for_each(|(index, mapping)| mount(mapping).map_err(|error| (index, error)))
})?;
Ok(table.is_complete())
}
}
Loading
Loading