Skip to content
Open
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
11 changes: 9 additions & 2 deletions nvx-rs/README.md

Large diffs are not rendered by default.

26 changes: 25 additions & 1 deletion nvx-rs/src/host/sandbox/distro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};

use super::geometry::BlockGeometry;
use super::layer::SandboxLayer;
use super::mount::os_error;
use super::refused;
use super::{block_geometry, refused};
use crate::error::{Error, Result};
use crate::host::artifacts::DISTRO_MANIFEST_SUFFIX;
use crate::host::network::policy::MAX_JSON_DEPTH;
Expand Down Expand Up @@ -213,6 +214,29 @@ impl CheckedDistro {
(self.format, &self.identity)
}

/// Returns the block geometry that OpenVMM presents for the layer as the read-only `distro`
/// block device, read through the checked file, as OpenVMM reads it through its own open of
/// the layer: the layer's length, which must be a nonzero multiple of 512 bytes; logical
/// blocks of 512 bytes; and physical blocks of the I/O block size of the layer's filesystem,
/// its `st_blksize`, on Linux, of 4096 bytes on Windows, and of the logical block size on
/// other hosts, which must be a power of two of at least 512 bytes.
///
/// It reads the file's metadata alone. A restore of a template compares the geometry of each
/// of its block devices with the one that the template recorded, so the image of a clone must
/// present the geometry of the template's `distro`, as
/// [`ClonePlan`](crate::host::template::ClonePlan) checks before the clone's launch.
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a
/// physical block size that OpenVMM refuses, and with [`ErrorKind::Io`](crate::ErrorKind::Io)
/// if the metadata cannot be read.
pub fn geometry(&self) -> Result<BlockGeometry> {
let description = match self.format {
DistroFormat::Erofs => "distro EROFS layer",
DistroFormat::Gpt => "distro GPT image",
};
block_geometry(&self.file, &self.path, description)
}

/// Returns the checked layer, for a prepared launch to move.
#[cfg(target_os = "linux")]
pub(crate) fn file_mut(&mut self) -> &mut File {
Expand Down
121 changes: 121 additions & 0 deletions nvx-rs/src/host/sandbox/distro/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,127 @@ fn the_staged_check_reads_no_content() {
assert_eq!(verify_with(&layer, Access::READ).unwrap(), binding);
}

/// Returns the physical block size that OpenVMM presents for the file at `path`: the I/O block
/// size of its filesystem on Linux, and 4096 bytes on Windows.
fn physical_block_size(path: &Path) -> u32 {
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::MetadataExt;

u32::try_from(fs::metadata(path).unwrap().blksize()).unwrap()
}
#[cfg(windows)]
{
let _ = path;
4096
}
}

#[test]
fn checked_layers_present_the_geometry_that_openvmm_reads_through_them() {
let staged = Staged::new(false);
let image = staged.image();
let resize = |length: u64| {
OpenOptions::new()
.write(true)
.open(&image)
.unwrap()
.set_len(length)
.unwrap();
};
let check = |layer: &SandboxLayer| {
import_with(layer, PolicyVerdict::ACCEPTED, METADATA_ONLY)
.unwrap()
.check_with(layer, DISTRO_POLICY_REVISION, METADATA_ONLY)
.unwrap()
};
resize(8192);
let checked = check(&staged.layer());
let geometry = BlockGeometry {
length: 8192,
logical_block_size: 512,
physical_block_size: physical_block_size(&image),
};
// It reads the metadata of the checked file alone, which reads nothing.
assert_eq!(checked.geometry().unwrap(), geometry);
// The geometry is the checked file's, whatever later takes its path.
#[cfg(target_os = "linux")]
{
let replacement = staged.other("replacement.erofs");
fs::write(&replacement, [0; 4096]).unwrap();
fs::rename(&replacement, &image).unwrap();
assert_eq!(checked.geometry().unwrap(), geometry);
}
drop(checked);
// OpenVMM attaches block devices of nonzero multiples of 512 bytes alone.
let gpt = SandboxLayer::gpt(LayerRole::Distro, image.to_str().unwrap(), UUID);
for (layer, description) in [
(staged.layer(), "distro EROFS layer"),
(gpt, "distro GPT image"),
] {
for length in [0, 8191] {
resize(length);
assert_eq!(
failure(check(&layer).geometry()),
(
ErrorKind::Unsupported,
format!(
"the {description} {} of {length} bytes cannot be attached: OpenVMM \
attaches block devices of nonzero multiples of 512 bytes",
shown(&image)
)
)
);
}
}
}

/// A layer on hugetlbfs, whose I/O block size is a huge page, presents physical blocks of 2 MiB.
#[cfg(target_os = "linux")]
#[test]
fn layers_present_the_block_size_of_their_filesystem() {
use nix::sys::memfd::{MFdFlags, memfd_create};

const HUGE_PAGE: u64 = 2 * 1024 * 1024;
let flags = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_HUGETLB | MFdFlags::MFD_HUGE_2MB;
let huge = match memfd_create("nvx-geometry-test", flags) {
Ok(descriptor) => File::from(descriptor),
Err(error) => {
eprintln!("this host offers no file on hugetlbfs: {error}");
return;
}
};
if let Err(error) = huge.set_len(HUGE_PAGE) {
eprintln!("this host cannot size a file on hugetlbfs: {error}");
return;
}
let staged = Staged::new(false);
let image = staged.image();
let open = |path: &Path| {
if path == image {
huge.try_clone()
} else {
Err(io::Error::from(io::ErrorKind::NotFound))
}
};
let access = Access {
open: &open,
seal: &FileSeal::of,
};
let checked = import_with(&staged.layer(), PolicyVerdict::ACCEPTED, access)
.unwrap()
.check_with(&staged.layer(), DISTRO_POLICY_REVISION, access)
.unwrap();
assert_eq!(
checked.geometry().unwrap(),
BlockGeometry {
length: HUGE_PAGE,
logical_block_size: 512,
physical_block_size: HUGE_PAGE as u32,
}
);
}

/// Stages a layer, with a manifest if `manifest` is true, binds it by verification, applies
/// `change`, and returns the failure of the check on a host where reads of content fail, with the
/// staged layer.
Expand Down
15 changes: 15 additions & 0 deletions nvx-rs/src/host/sandbox/geometry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! The block geometry of a sandbox block device, which the checks of its files read as OpenVMM
//! presents it, and which a template records for the devices of its capture.
//! [`template`](crate::host::template) re-exports what is public here.

/// The length and block sizes of a sandbox block device.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BlockGeometry {
/// The length that the device presents, in bytes: nonzero, and a multiple of the logical
/// block size.
pub length: u64,
/// The logical block size, in bytes: a power of two of at least 512.
pub logical_block_size: u32,
/// The physical block size, in bytes: a power of two of at least the logical block size.
pub physical_block_size: u32,
}
66 changes: 66 additions & 0 deletions nvx-rs/src/host/sandbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
//! layer and its manifest, and which a launch checks from metadata alone.
//! - [`scratch`]: the binding of the scratch image, its identity, kind, and length, which a
//! launch checks from metadata alone, and the claim of the image by one sandbox at a time.
//! - The block geometry that OpenVMM presents for each checked file, its length and its logical
//! and physical block sizes, which [`CheckedDistro::geometry`](distro::CheckedDistro::geometry)
//! and [`CheckedScratch::geometry`](scratch::CheckedScratch::geometry) read through the file,
//! as OpenVMM reads it.
//! - [`PreparedLaunch`]: the launch of a plan with the files that the checks of its block devices
//! returned, which hands the VMM those objects rather than paths that something could replace
//! meanwhile: the OpenVMM arguments that name them, and the descriptors that the VMM inherits;
Expand All @@ -49,17 +53,21 @@
//! the path quoted with Rust's debug formatting.

pub mod distro;
pub(crate) mod geometry;
pub mod layer;
pub mod mount;
pub mod options;
pub mod plan;
pub mod prepared;
pub mod scratch;

use std::fs::File;
use std::path::Path;

use self::distro::{DistroBinding, DistroFormat};
use self::geometry::BlockGeometry;
pub use self::layer::{LAYER_ROLES, LayerRole, SandboxLayer};
use self::mount::os_error;
pub use self::mount::{MountAccess, MountKind, MountOwner, MountPolicy, PolicyKind, SandboxMount};
pub use self::plan::{
BlockRole, PLAN_VERSION, SandboxDistro, SandboxNetwork, SandboxPlan, SandboxUpper,
Expand Down Expand Up @@ -189,6 +197,64 @@ pub(crate) fn reject_container_path(path: &Path, shown: &str) -> Result<()> {
Ok(())
}

/// The logical block size that OpenVMM presents for a sandbox block device, in bytes, of which the
/// device's length is a nonzero multiple.
const LOGICAL_BLOCK_SIZE: u32 = 512;

/// The physical block size that OpenVMM presents for a sandbox block device on Windows, in bytes.
#[cfg(windows)]
const WINDOWS_PHYSICAL_BLOCK_SIZE: u32 = 4096;

/// Returns the block geometry that OpenVMM presents for a sandbox block device whose file `file`
/// holds open, which messages call `description` at `path`, read through `file` as OpenVMM reads
/// it through its own open of the file, as
/// [`CheckedDistro::geometry`](distro::CheckedDistro::geometry) describes.
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a physical
/// block size that OpenVMM refuses, and with [`ErrorKind::Io`](crate::ErrorKind::Io) if the file's
/// metadata cannot be read.
pub(crate) fn block_geometry(file: &File, path: &Path, description: &str) -> Result<BlockGeometry> {
let shown = path.display();
let metadata = file
.metadata()
.map_err(|error| os_error(&error, Some(path)))?;
let length = metadata.len();
if length == 0 || !length.is_multiple_of(u64::from(LOGICAL_BLOCK_SIZE)) {
return Err(refused(format!(
"the {description} {shown} of {length} bytes cannot be attached: OpenVMM attaches \
block devices of nonzero multiples of {LOGICAL_BLOCK_SIZE} bytes"
)));
}
#[cfg(target_os = "linux")]
let (reported, physical) = {
use std::os::unix::fs::MetadataExt;

let size = metadata.blksize();
(size, u32::try_from(size).ok())
};
#[cfg(windows)]
let (reported, physical) = (
u64::from(WINDOWS_PHYSICAL_BLOCK_SIZE),
Some(WINDOWS_PHYSICAL_BLOCK_SIZE),
);
#[cfg(not(any(target_os = "linux", windows)))]
let (reported, physical) = (u64::from(LOGICAL_BLOCK_SIZE), Some(LOGICAL_BLOCK_SIZE));
match physical {
Some(physical) if physical >= LOGICAL_BLOCK_SIZE && physical.is_power_of_two() => {
Ok(BlockGeometry {
length,
logical_block_size: LOGICAL_BLOCK_SIZE,
physical_block_size: physical,
})
}
_ => Err(refused(format!(
"the {description} {shown} cannot be attached: its filesystem's block size of \
{reported} bytes is not a physical block size that OpenVMM presents, a power of two \
from {LOGICAL_BLOCK_SIZE} bytes to 2 GiB"
))),
}
}

/// The launch settings of a [`SandboxPlan`] or a [`SandboxLaunch`], besides its block devices and
/// its shares, with the defaults of `nvx-rs sandbox`.
#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down
22 changes: 21 additions & 1 deletion nvx-rs/src/host/sandbox/scratch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ use std::time::{Duration, Instant};

use serde::{Deserialize, Serialize};

use super::geometry::BlockGeometry;
use super::mount::os_error;
use super::refused;
use super::{block_geometry, refused};
use crate::error::{Error, ErrorCode, Result};
use crate::host::paths;
use crate::host::seal::{FileId, FileKind, FileSeal, SealScheme};
Expand Down Expand Up @@ -157,6 +158,25 @@ impl CheckedScratch {
&self.path
}

/// Returns the block geometry that OpenVMM presents for the image as the `scratch` block
/// device, read through the checked file, as
/// [`CheckedDistro::geometry`](super::distro::CheckedDistro::geometry) reads that of a distro
/// layer: the image's length, logical blocks of 512 bytes, and physical blocks of the I/O
/// block size of the image's filesystem on Linux, of 4096 bytes on Windows, and of the logical
/// block size on other hosts. On Windows, the checked file is the image's guard, which may
/// read the image's metadata.
///
/// It reads the file's metadata alone, never the image's content. The scratch image of a
/// clone must present the geometry of its template's scratch, its length included, as
/// [`ClonePlan`](crate::host::template::ClonePlan) checks before the clone's launch.
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a
/// physical block size that OpenVMM refuses, and with [`ErrorKind::Io`](crate::ErrorKind::Io)
/// if the metadata cannot be read.
pub fn geometry(&self) -> Result<BlockGeometry> {
block_geometry(&self.file, &self.path, DESCRIPTION)
}

/// Returns the checked image, which the caller then holds with its claim: an owned descriptor
/// on Linux, and on Windows an owned handle, the image's guard, which then holds the claim
/// alone, since this closes the claim's handle.
Expand Down
62 changes: 62 additions & 0 deletions nvx-rs/src/host/sandbox/scratch/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,68 @@ fn checks_claim_the_image_without_reading_it() {
drop(staged.claim(&binding));
}

/// Returns the physical block size that OpenVMM presents for the file at `path`: the I/O block
/// size of its filesystem on Linux, and 4096 bytes on Windows.
fn physical_block_size(path: &Path) -> u32 {
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::MetadataExt;

u32::try_from(fs::metadata(path).unwrap().blksize()).unwrap()
}
#[cfg(windows)]
{
let _ = path;
4096
}
}

#[test]
fn checked_images_present_the_geometry_that_openvmm_reads_through_them() {
let staged = Staged::new();
let resize = |length: u64| {
OpenOptions::new()
.write(true)
.open(staged.image())
.unwrap()
.set_len(length)
.unwrap();
};
resize(1 << 20);
let binding = staged.bind();
let checked = staged.claim(&binding);
// It reads the metadata of what the check holds, which reads nothing, while the claim lasts.
assert_eq!(
checked.geometry().unwrap(),
BlockGeometry {
length: 1 << 20,
logical_block_size: 512,
physical_block_size: physical_block_size(&staged.image()),
}
);
assert_eq!(
failure(staged.check(&binding, Instant::now())),
claimed_failure(&staged.image())
);
drop(checked);
// OpenVMM attaches block devices of nonzero multiples of 512 bytes alone.
for length in [0, CONTENT.len() as u64, (1 << 20) + 1] {
resize(length);
let binding = staged.bind();
assert_eq!(
failure(staged.claim(&binding).geometry()),
(
ErrorKind::Unsupported,
format!(
"the ext4 scratch image {} of {length} bytes cannot be attached: OpenVMM \
attaches block devices of nonzero multiples of 512 bytes",
shown(&staged.image())
)
)
);
}
}

#[test]
fn a_claim_lasts_while_the_vmm_holds_the_image() {
let staged = Staged::new();
Expand Down
Loading
Loading