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
18 changes: 14 additions & 4 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ changes only when the module's purpose does.
| [`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, cold preparation, verified detached launch, confirmed stop, and caller-saved reattachment |
| [`nvx::host::snapshot`](#nvxhostsnapshot) | Snapshot compatibility keys, strict manifests, restore-time eligibility, and the opaque input of a future VM restore |
| [`nvx::host::snapshot`](#nvxhostsnapshot) | Strict private snapshot directories, verified keys and manifests, time eligibility, and the 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 |
| [`nvx::host::artifacts`](#nvxhostartifacts) | Locating the OpenVMM executable and the guest artifacts that a VM runs, and their runtime identity |
Expand Down Expand Up @@ -579,9 +579,19 @@ The key's canonical encoding and identity use the snapshot names and omit
and no reader or migration of the prior encoding is supported. `CaptureClass`
still records the legacy capture inputs, including its guest build.

`Snapshot` is an opaque input of a future restore. `vm::create` currently
refuses `Some(snapshot)` with `ErrorKind::Unsupported`; only cold boots are
supported.
`Snapshot::open` checks exactly `key.bin`, `snapshot.json` and
`openvmm/{manifest.bin,memory.bin,state.bin}`. Directories are private, files
are regular and private to their owner, artifact lengths equal the strict
manifest's, and the canonical key is consistent with that manifest.
`KEY_FILE`, `DESCRIPTION_FILE` and `OPENVMM_DIR` name the root entries.
The description must exist, but its contents are never read, parsed or
trusted. `Snapshot::key` and `manifest` expose the verified metadata.

The caller keeps the directory and every entry private and immutable while
using the snapshot; opening checks them once and retains no file handles
or lock. `Snapshot` remains an opaque input of a future restore:
`vm::create` still refuses `Some(snapshot)` with `ErrorKind::Unsupported`;
only cold boots are supported.

### `nvx::host::config`

Expand Down
6 changes: 6 additions & 0 deletions nvx-rs/public-api/linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3040,6 +3040,7 @@ method nvx::host::snapshot::CaptureClass::to_bytes
struct nvx::host::snapshot::ClassIdentity
method nvx::host::snapshot::ClassIdentity::as_bytes
method nvx::host::snapshot::ClassIdentity::from_bytes
const nvx::host::snapshot::DESCRIPTION_FILE
struct nvx::host::snapshot::Downtime
field nvx::host::snapshot::Downtime::nanos
field nvx::host::snapshot::Downtime::source
Expand Down Expand Up @@ -3077,13 +3078,18 @@ method nvx::host::snapshot::Invariants::iter
method nvx::host::snapshot::Invariants::len
method nvx::host::snapshot::Invariants::new
method nvx::host::snapshot::Invariants::with
const nvx::host::snapshot::KEY_FILE
struct nvx::host::snapshot::KeyIdentity
method nvx::host::snapshot::KeyIdentity::as_bytes
method nvx::host::snapshot::KeyIdentity::from_bytes
const nvx::host::snapshot::MANIFEST_NAME
const nvx::host::snapshot::MAX_DOWNTIME_NS
const nvx::host::snapshot::MAX_MANIFEST_LEN
const nvx::host::snapshot::OPENVMM_DIR
struct nvx::host::snapshot::Snapshot
method nvx::host::snapshot::Snapshot::key
method nvx::host::snapshot::Snapshot::manifest
method nvx::host::snapshot::Snapshot::open
struct nvx::host::snapshot::SnapshotKey
const nvx::host::snapshot::SnapshotKey::MAX_LEN
const nvx::host::snapshot::SnapshotKey::VERSION
Expand Down
6 changes: 6 additions & 0 deletions nvx-rs/public-api/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,7 @@ method nvx::host::snapshot::CaptureClass::to_bytes
struct nvx::host::snapshot::ClassIdentity
method nvx::host::snapshot::ClassIdentity::as_bytes
method nvx::host::snapshot::ClassIdentity::from_bytes
const nvx::host::snapshot::DESCRIPTION_FILE
struct nvx::host::snapshot::Downtime
field nvx::host::snapshot::Downtime::nanos
field nvx::host::snapshot::Downtime::source
Expand Down Expand Up @@ -1663,13 +1664,18 @@ method nvx::host::snapshot::Invariants::iter
method nvx::host::snapshot::Invariants::len
method nvx::host::snapshot::Invariants::new
method nvx::host::snapshot::Invariants::with
const nvx::host::snapshot::KEY_FILE
struct nvx::host::snapshot::KeyIdentity
method nvx::host::snapshot::KeyIdentity::as_bytes
method nvx::host::snapshot::KeyIdentity::from_bytes
const nvx::host::snapshot::MANIFEST_NAME
const nvx::host::snapshot::MAX_DOWNTIME_NS
const nvx::host::snapshot::MAX_MANIFEST_LEN
const nvx::host::snapshot::OPENVMM_DIR
struct nvx::host::snapshot::Snapshot
method nvx::host::snapshot::Snapshot::key
method nvx::host::snapshot::Snapshot::manifest
method nvx::host::snapshot::Snapshot::open
struct nvx::host::snapshot::SnapshotKey
const nvx::host::snapshot::SnapshotKey::MAX_LEN
const nvx::host::snapshot::SnapshotKey::VERSION
Expand Down
189 changes: 185 additions & 4 deletions nvx-rs/src/host/snapshot.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
//! Snapshot metadata and the opaque input of a future VM restore.
//! Strict snapshot directories, compatibility keys, manifests and restore-time eligibility.
//!
//! [`Snapshot::open`] checks the one snapshot format without reading its informational
//! description. The caller keeps the directory and its entries private and immutable while
//! using the opened snapshot.
//!
//! [`SnapshotKey`] derives a compatibility key from a capture's class and manifest.
//! [`SnapshotManifest::read`] reads the manifest that OpenVMM wrote, and
Expand Down Expand Up @@ -31,15 +35,192 @@ pub(super) mod manifest;
pub(crate) mod tests;
mod wire;

use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use crate::host::paths::{absolute, normalized};
use crate::host::store::regular;
use crate::host::{filesystem, platform};
use crate::{Error, Result};

pub use self::eligibility::{Downtime, DowntimeSource, Eligibility, HostTime, MAX_DOWNTIME_NS};
pub use self::key::{CaptureClass, ClassIdentity, Invariants, KeyIdentity, SnapshotKey};
pub use self::manifest::{
BlockGeometry, HostClock, MANIFEST_NAME, MAX_MANIFEST_LEN, SnapshotManifest, TimeContract,
};

/// An opaque snapshot input. Creating a VM from a snapshot is not supported yet.
/// The canonical snapshot key.
pub const KEY_FILE: &str = "key.bin";
/// The informational description, whose contents [`Snapshot::open`] never reads.
pub const DESCRIPTION_FILE: &str = "snapshot.json";
/// The directory of OpenVMM's manifest, RAM and saved state.
pub const OPENVMM_DIR: &str = "openvmm";

/// A checked snapshot directory and its verified metadata, for a future VM restore.
///
/// The caller must keep the directory and its entries private and immutable while using this
/// value. Opening checks them once; it neither watches them nor holds their files open.
#[derive(Debug)]
pub struct Snapshot {
/// Reserved for snapshot metadata.
pub(crate) _private: (),
/// The absolute directory path, with its components preserved rather than its links resolved.
#[cfg_attr(
not(test),
expect(dead_code, reason = "SL2.15 uses the snapshot directory (#1103)")
)]
pub(crate) dir: PathBuf,
/// The bounded canonical key checked against the manifest.
key: SnapshotKey,
/// The strictly parsed metadata of OpenVMM's snapshot.
manifest: SnapshotManifest,
}

impl Snapshot {
/// Opens the one strict snapshot format without changing any entry.
///
/// `dir` must be a private directory containing exactly [`KEY_FILE`], [`DESCRIPTION_FILE`]
/// and [`OPENVMM_DIR`]. The latter must be a private directory containing exactly
/// [`MANIFEST_NAME`], `memory.bin` and `state.bin`. Every file must be regular, not a link or
/// reparse point, and private to its owner. The manifest is read by
/// [`SnapshotManifest::read`], its artifact lengths must match, and the bounded canonical
/// key must be consistent with it. The description is required but its contents are never
/// read, parsed or trusted.
///
/// The caller must keep the directory and its entries private and immutable while using
/// the returned snapshot. This check retains no open file handles or lock.
///
/// Fails with [`ErrorKind::Io`](crate::ErrorKind::Io) for an unreadable or invalid directory,
/// entry, length or key relationship, and otherwise as [`SnapshotManifest::read`] and
/// [`SnapshotKey::from_bytes`] do.
pub fn open(dir: &Path) -> Result<Self> {
let dir = absolute(&normalized(dir)).map_err(|error| entry_error(dir, error))?;
check_directory(&dir, &[KEY_FILE, DESCRIPTION_FILE, OPENVMM_DIR])?;
let openvmm = dir.join(OPENVMM_DIR);
check_directory(&openvmm, &[MANIFEST_NAME, "memory.bin", "state.bin"])?;
for path in [
dir.join(KEY_FILE),
dir.join(DESCRIPTION_FILE),
openvmm.join(MANIFEST_NAME),
] {
check_file(&path)?;
}
let manifest = SnapshotManifest::read(&openvmm)?;
for (name, expected) in [
("memory.bin", manifest.memory_len()),
("state.bin", manifest.state_len()),
] {
let path = openvmm.join(name);
let found = check_file(&path)?.len();
if found != expected {
return Err(Error::io(format!(
"the snapshot artifact {} is {found} bytes long, not the {expected} bytes \
that its manifest records",
path.display()
)));
}
}
let path = dir.join(KEY_FILE);
let bytes = regular::open_regular(&path)
.and_then(|file| {
regular::read_at_most(file, SnapshotKey::MAX_LEN as u64, "a snapshot key")
})
.map_err(|error| entry_error(&path, error))?;
let key = SnapshotKey::from_bytes(&bytes)?;
key.check_manifest(&manifest)?;
Ok(Self { dir, key, manifest })
}

/// Returns the verified snapshot key.
pub fn key(&self) -> &SnapshotKey {
&self.key
}

/// Returns the strictly parsed OpenVMM manifest.
pub fn manifest(&self) -> &SnapshotManifest {
&self.manifest
}
}

/// Names the failing snapshot entry while retaining its I/O error.
fn entry_error(path: &Path, error: io::Error) -> Error {
Error::io(format!("cannot examine snapshot entry {}", path.display())).with_source(error)
}

/// Checks one private directory and its exact fixed entry-name inventory.
fn check_directory(path: &Path, expected: &[&str]) -> Result<()> {
if let Some(problem) =
platform::check_snapshot_dir(path).map_err(|error| entry_error(path, error))?
{
return Err(Error::io(format!(
"the snapshot directory {} {problem}",
path.display()
)));
}
let mut found = fs::read_dir(path)
.and_then(|entries| {
entries
.map(|entry| Ok(entry?.file_name()))
.collect::<io::Result<Vec<_>>>()
})
.map_err(|error| entry_error(path, error))?;
let mut expected: Vec<OsString> = expected.iter().map(|name| OsString::from(*name)).collect();
found.sort();
expected.sort();
if found != expected {
return Err(Error::io(format!(
"the snapshot directory {} holds {found:?}, not exactly {expected:?}",
path.display()
)));
}
Ok(())
}

/// Checks a regular private file from metadata alone, without reading its contents.
fn check_file(path: &Path) -> Result<fs::Metadata> {
#[cfg(windows)]
let file = {
use std::os::windows::fs::OpenOptionsExt;
use windows_sys::Win32::Storage::FileSystem::{
FILE_FLAG_OPEN_REPARSE_POINT, FILE_READ_ATTRIBUTES, READ_CONTROL,
};

fs::OpenOptions::new()
.access_mode(READ_CONTROL | FILE_READ_ATTRIBUTES)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
.map_err(|error| entry_error(path, error))?
};
#[cfg(windows)]
let metadata = file.metadata().map_err(|error| entry_error(path, error))?;
#[cfg(not(windows))]
let metadata = fs::symlink_metadata(path).map_err(|error| entry_error(path, error))?;
if !metadata.is_file() || filesystem::is_reparse_point(&metadata) {
return Err(Error::io(format!(
"the snapshot entry {} is not a regular file, or is a link or reparse point",
path.display()
)));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;

if metadata.uid() != nix::unistd::Uid::effective().as_raw() || metadata.mode() & 0o077 != 0
{
return Err(Error::io(format!(
"the snapshot file {} is not private to the current user",
path.display()
)));
}
}
#[cfg(windows)]
if let Some(problem) =
platform::private_file_problem(&file).map_err(|error| entry_error(path, error))?
{
return Err(Error::io(format!(
"the snapshot file {} {problem}",
path.display()
)));
}
Ok(metadata)
}
13 changes: 13 additions & 0 deletions nvx-rs/src/host/snapshot/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,19 @@ impl SnapshotKey {
self.contracts == Contracts::CURRENT
}

/// Checks the manifest's complete machine, the current key contracts and the filesystem tag.
pub(super) fn check_manifest(&self, manifest: &SnapshotManifest) -> Result<()> {
if self.machine != manifest.machine
|| !self.speaks_current_contracts()
|| self.filesystem_tag != MOUNT_TAG
{
return Err(Error::io(
"the snapshot key is not consistent with its manifest and this format's contracts",
));
}
Ok(())
}

/// Returns the backend.
pub fn hypervisor(&self) -> Hypervisor {
self.machine.hypervisor
Expand Down
23 changes: 23 additions & 0 deletions nvx-rs/src/host/snapshot/key/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,29 @@ fn key_refusal(bytes: &[u8], code: ErrorCode) -> String {
error.message().to_owned()
}

#[cfg(any(target_os = "linux", windows))]
#[test]
fn snapshot_open_refuses_other_key_contracts_and_filesystem_tags() {
use crate::host::snapshot::{KEY_FILE, Snapshot};
use crate::host::store::files::write_private;

let changes: [fn(&mut SnapshotKey); 2] = [
|key| key.contracts.snapshot += 1,
|key| key.filesystem_tag.push_str("-other"),
];
for change in changes {
let fixture = support::SnapshotFixture::new();
let mut key = Snapshot::open(&fixture.dir).unwrap().key().clone();
change(&mut key);
let bytes = key.to_bytes();
assert!(SnapshotKey::from_bytes(&bytes).is_ok());
write_private(&fixture.dir.join(KEY_FILE), &bytes).unwrap();
let error = Snapshot::open(&fixture.dir).unwrap_err();
assert_eq!(error.code(), ErrorCode::Io, "{error}");
assert!(error.message().contains("not consistent"), "{error}");
}
}

#[test]
fn other_encodings_of_a_key_are_refused() {
let key = network_key();
Expand Down
Loading
Loading