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
3 changes: 3 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ each code for callers of the `nvx` library.
| `foreign-files` | 1 | `deprovision` found files that NVX does not own in the state directory, which the message names, and kept them with the directory. |
| `outcome-report-exists` | 1 | A file exists at the `--outcome-report` path. |
| `scratch-claimed` | 1 | Another sandbox claims the scratch image. |
| `sandbox-leased` | 1 | The sandbox holds a lease of a template's generation already, which a release ends once its VM has exited. |
| `template-ineligible` | 1 | The restore window of a template's generation, which the time contract of its capture sets, excludes the host's time, so no clone may restore it now. |
| `template-tombstoned` | 1 | A tombstone quarantines the template's generation, which takes no new clones. |
| `exited` | 1 | OpenVMM exited, as when a start fails, whose message names the category and status of OpenVMM's outcome report when it has one. |
| `exec-refused` | 1 | The guest agent refused the workload, as for a working directory that it cannot use. |
| `endpoint-closed` | 1 | OpenVMM closed the control endpoint, as it does when the VM shuts down. |
Expand Down
3 changes: 3 additions & 0 deletions nvx-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ sha2 = { version = "0.10", optional = true }

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", optional = true, features = [
"Wdk_Foundation",
"Wdk_Storage_FileSystem",
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
Expand All @@ -68,6 +70,7 @@ windows-sys = { version = "0.61", optional = true, features = [
"Win32_System_Pipes",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_System_WindowsProgramming",
] }

# The host and the fake OpenVMM also build on Unix hosts other than Linux, such as macOS.
Expand Down
15 changes: 9 additions & 6 deletions nvx-rs/README.md

Large diffs are not rendered by default.

32 changes: 26 additions & 6 deletions nvx-rs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ pub enum ErrorCode {
/// this build does not read, such as the state of a sandbox that another build provisioned or
/// started.
FormatUnsupported,
/// `foreign-files`: the sandbox's state directory holds files that NVX does not own, which a
/// removal keeps.
/// `foreign-files`: a directory of NVX's state, such as a sandbox's state directory or a
/// template store, holds files that NVX does not own, which the message names, and which NVX
/// keeps.
ForeignFiles,
/// `outcome-report-exists`: a file exists at the path of the outcome report to write.
OutcomeReportExists,
Expand All @@ -146,6 +147,16 @@ pub enum ErrorCode {
ExecRefused,
/// `endpoint-closed`: OpenVMM closed the control endpoint, as it does when the VM shuts down.
EndpointClosed,
/// `template-ineligible`: the restore window of a template's generation, which the time
/// contract of its capture sets, excludes the host's time, so no clone may restore it now: its
/// capture is too old, or the host's clock is behind the capture's.
TemplateIneligible,
/// `template-tombstoned`: a tombstone quarantines the template's generation, which takes no
/// new clones, while the clones that lease it keep their leases.
TemplateTombstoned,
/// `sandbox-leased`: the sandbox holds a lease of a template's generation already, which a
/// release ends once its VM has exited.
SandboxLeased,
}

impl ErrorCode {
Expand Down Expand Up @@ -178,6 +189,9 @@ impl ErrorCode {
Self::ScratchClaimed,
Self::ExecRefused,
Self::EndpointClosed,
Self::TemplateIneligible,
Self::TemplateTombstoned,
Self::SandboxLeased,
];

/// Returns the stable spelling of this code.
Expand Down Expand Up @@ -210,15 +224,20 @@ impl ErrorCode {
Self::ScratchClaimed => "scratch-claimed",
Self::ExecRefused => "exec-refused",
Self::EndpointClosed => "endpoint-closed",
Self::TemplateIneligible => "template-ineligible",
Self::TemplateTombstoned => "template-tombstoned",
Self::SandboxLeased => "sandbox-leased",
}
}

/// Returns the kind of the errors that have this code.
pub const fn kind(self) -> ErrorKind {
match self {
Self::Unavailable | Self::ArtifactMissing | Self::GuestFeaturesMissing => {
ErrorKind::Unavailable
}
Self::Unavailable
| Self::ArtifactMissing
| Self::GuestFeaturesMissing
| Self::TemplateIneligible
| Self::TemplateTombstoned => ErrorKind::Unavailable,
Self::Unsupported
| Self::ArgumentUnrecognized
| Self::ArgumentMissing
Expand All @@ -235,7 +254,8 @@ impl ErrorCode {
| Self::RuntimeMismatch
| Self::VmmIndeterminate
| Self::ForeignFiles
| Self::OutcomeReportExists => ErrorKind::Other,
| Self::OutcomeReportExists
| Self::SandboxLeased => ErrorKind::Other,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion nvx-rs/src/host/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1972,7 +1972,7 @@ fn depth(path: &str) -> usize {
/// name, among other things. A path that resolves to itself again names the same object; one that
/// only the verbatim prefix keeps, such as that of a directory whose name ends in a period, would
/// name another object, or none, so it is refused.
fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
pub(crate) fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
let canonical = fs::canonicalize(path)?;
let Some(stripped) = strip_verbatim(&canonical) else {
return Err(std::io::Error::new(
Expand Down
2 changes: 1 addition & 1 deletion nvx-rs/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
//! compatibility key of a template, [`TemplateKey`](template::TemplateKey), which it derives
//! from the [`CaptureClass`](template::CaptureClass) of the capture and the capture's
//! [`TemplateManifest`](template::TemplateManifest), and the [`Store`](template::Store) of the
//! generations of templates that a host published.
//! generations of templates that a host published, which leases them to clones.
//! - [`session`] speaks NVX's control protocol, `NVXC`, over [`nvxs`], which runs the host's role
//! of the NVXS session for it and for any other application protocol, over a platform
//! transport. [`platform`] holds the primitives of Linux and Windows hosts. Other hosts compile
Expand Down
80 changes: 80 additions & 0 deletions nvx-rs/src/host/platform/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,35 @@ pub(crate) fn boot_id() -> io::Result<[u8; 16]> {
})
}

/// The file of the host's machine ID.
const MACHINE_ID_PATH: &str = "/etc/machine-id";

/// Returns the identity of this host, as OpenVMM records it in a capture's time contract: its
/// machine ID, from [`MACHINE_ID_PATH`], as 16 bytes in the order in which it is written.
pub(crate) fn host_id() -> io::Result<[u8; 16]> {
let text = fs::read_to_string(MACHINE_ID_PATH)?;
super::parse_host_id(&text).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{MACHINE_ID_PATH} has an unexpected format"),
)
})
}

/// Returns the host's monotonic time, as OpenVMM measures the downtime of a capture within one
/// boot: `CLOCK_BOOTTIME`, in nanoseconds, which counts the time that the host was suspended.
pub(crate) fn monotonic_ns() -> io::Result<u64> {
/// Nanoseconds in a second.
const NANOS_PER_SECOND: u64 = 1_000_000_000;
let time = nix::time::clock_gettime(nix::time::ClockId::CLOCK_BOOTTIME)?;
u64::try_from(time.tv_sec())
.ok()
.and_then(|seconds| seconds.checked_mul(NANOS_PER_SECOND))
.zip(u64::try_from(time.tv_nsec()).ok())
.and_then(|(seconds, nanos)| seconds.checked_add(nanos))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "CLOCK_BOOTTIME is out of range"))
}

/// Starts the command in a new session so it outlives the caller and its terminal, with the
/// descriptors `inherit` under their numbers in the caller, and no other descriptor of the caller
/// beyond the standard streams that the command sets up.
Expand Down Expand Up @@ -550,12 +579,63 @@ pub fn file_identity(path: &Path) -> io::Result<(u64, u64)> {
Ok((metadata.dev(), metadata.ino()))
}

/// Returns whether the file at `path` is deleted: whether no file has that name, which a removal
/// takes at once.
pub(crate) fn is_deleted(path: &Path) -> bool {
fs::symlink_metadata(path).is_err_and(|error| error.kind() == io::ErrorKind::NotFound)
}

/// Returns the device and inode of the file that `file` holds open.
pub(crate) fn opened_file_identity(file: &File) -> io::Result<(u64, u64)> {
let metadata = file.metadata()?;
Ok((metadata.dev(), metadata.ino()))
}

/// An open directory, through which operations name its entries, so that they reach the entries
/// of the directory that it opened, whatever its path names meanwhile.
pub(crate) struct Directory(File);

impl Directory {
/// Opens the directory `path`, following its links.
pub(crate) fn open(path: &Path) -> io::Result<Self> {
use nix::fcntl::OFlag;

OpenOptions::new()
.read(true)
.custom_flags(OFlag::O_DIRECTORY.bits())
.open(path)
.map(Self)
}

/// Returns the device and inode of its entry `name`, not following a link, as
/// [`opened_file_identity`] gives them, or `None` if it has no such entry.
pub(crate) fn entry_identity(&self, name: &str) -> io::Result<Option<(u64, u64)>> {
use nix::fcntl::AtFlags;
use nix::sys::stat::fstatat;

match fstatat(&self.0, name, AtFlags::AT_SYMLINK_NOFOLLOW) {
Ok(status) => Ok(Some((status.st_dev, status.st_ino))),
Err(Errno::ENOENT) => Ok(None),
Err(errno) => Err(errno.into()),
}
}

/// Removes its file `name`, if it has one.
pub(crate) fn remove_file(&self, name: &str) -> io::Result<()> {
use nix::unistd::{UnlinkatFlags, unlinkat};

match unlinkat(&self.0, name, UnlinkatFlags::NoRemoveDir) {
Ok(()) | Err(Errno::ENOENT) => Ok(()),
Err(errno) => Err(errno.into()),
}
}

/// Makes the changes to its entries so far durable, such as a file that was removed from it.
pub(crate) fn sync(&self) -> io::Result<()> {
self.0.sync_all()
}
}

/// Claims a launch's fresh log, which `file` holds open.
///
/// The claim is an exclusive `flock` lock. It belongs to the open file description, which the
Expand Down
59 changes: 59 additions & 0 deletions nvx-rs/src/host/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,62 @@ pub(crate) fn parse_hex_id(digits: &str) -> Option<[u8; 16]> {
}
Some(id)
}

/// Parses the identity of a host as OpenVMM reads it: 32 hexadecimal digits, in either case,
/// between optional braces and surrounding whitespace, ignoring dashes, as in `/etc/machine-id`
/// and the Windows machine GUID.
#[cfg(any(target_os = "linux", windows))]
fn parse_host_id(text: &str) -> Option<[u8; 16]> {
let digits: String = text
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
.chars()
.filter(|&character| character != '-')
.collect();
parse_hex_id(&digits)
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(any(target_os = "linux", windows))]
#[test]
fn host_identities_are_read_as_openvmm_reads_them() {
let id = [
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef,
];
for text in [
"0123456789abcdef0123456789ABCDEF\n",
"01234567-89ab-cdef-0123-456789abcdef",
"{01234567-89AB-CDEF-0123-456789ABCDEF}",
" 0123456789abcdef0123456789abcdef ",
] {
assert_eq!(parse_host_id(text), Some(id), "{text:?}");
}
for text in [
"0123",
"0123456789abcdef0123456789abcdeg",
"0123456789abcdef0123456789abcdef00",
"",
] {
assert_eq!(parse_host_id(text), None, "{text:?}");
}
}

#[cfg(any(target_os = "linux", windows))]
#[test]
fn the_hosts_identity_and_monotonic_time_are_read() {
let first = monotonic_ns().unwrap();
assert!(monotonic_ns().unwrap() >= first);
match host_id() {
Ok(id) => assert_eq!(host_id().unwrap(), id),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
eprintln!("skipped: this host has no machine identity: {error}");
}
Err(error) => panic!("{error}"),
}
}
}
41 changes: 41 additions & 0 deletions nvx-rs/src/host/platform/other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,56 @@ pub(crate) fn boot_id() -> io::Result<[u8; 16]> {
Err(unsupported())
}

/// Fails: this host has no host identity that NVX reads.
pub(crate) fn host_id() -> io::Result<[u8; 16]> {
Err(unsupported())
}

/// Fails: this host has no monotonic clock that NVX reads.
pub(crate) fn monotonic_ns() -> io::Result<u64> {
Err(unsupported())
}

/// Fails: this host cannot export host paths.
pub fn file_identity(_path: &Path) -> io::Result<(u64, u64)> {
Err(unsupported())
}

/// Returns whether the file at `path` is deleted: whether no file has that name, which a removal
/// takes at once.
pub(crate) fn is_deleted(path: &Path) -> bool {
fs::symlink_metadata(path).is_err_and(|error| error.kind() == io::ErrorKind::NotFound)
}

pub(crate) fn opened_file_identity(_file: &File) -> io::Result<(u64, u64)> {
Err(unsupported())
}

/// An open directory, which this host never holds: it keeps no sandbox's store.
pub(crate) enum Directory {}

impl Directory {
/// Fails: this host keeps no sandbox's store.
pub(crate) fn open(_path: &Path) -> io::Result<Self> {
Err(unsupported())
}

/// Never called: no directory is open.
pub(crate) fn entry_identity(&self, _name: &str) -> io::Result<Option<(u64, u64)>> {
match *self {}
}

/// Never called: no directory is open.
pub(crate) fn remove_file(&self, _name: &str) -> io::Result<()> {
match *self {}
}

/// Never called: no directory is open.
pub(crate) fn sync(&self) -> io::Result<()> {
match *self {}
}
}

pub(crate) fn claim_log(_file: &File) -> io::Result<()> {
Err(unsupported())
}
Expand Down
Loading
Loading