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
57 changes: 33 additions & 24 deletions aci_edge_sandboxes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ approved, and a `SetupConfig`, which the library applies once per process:
},
// How long a library that pulls registry references may take to materialize one.
"images": { "references": { "pullTimeoutMs": 1800000 } },
"diagnostics": { "guestDebug": false, "contentVerification": false }
"diagnostics": { "guestDebug": false }
}
```

Expand All @@ -170,17 +170,18 @@ runtime ABI. The library checks every file but itself: the caller passes the lib
`RuntimeFiles::approved_sha256`.

A `SandboxSpec`, passed to `AciEdgeSandbox::provision_with` beside the provision request, sets
one sandbox's image (`ImageSource::Path` of a local GPT disk, `ImageSource::Digest` of a
registered image, or `ImageSource::Reference` of a registry image where the library supports it),
its resources, its guest network, and its forwarded ports. Every guest has one
one sandbox's image (`ImageSource::Path` of a registered local GPT disk, `ImageSource::Digest` of
a registered image, or `ImageSource::Reference` of a registered registry image where the library
supports it), its resources, its guest network, and its forwarded ports. Every guest has one
virtual processor, so `resources.vcpus` may only be 1, and `resources.memoryMib` sets its memory.
`Capabilities::spec` lists the fields that the library honors; hostnames are not among them yet.
A request may still set `microvm.provision.memoryMib`, but not together
with `spec.resources.memoryMib`.

A library that honors `spec.image.reference` (`Capabilities::spec.image_reference`) converts a
reference into a local disk image with an external tool the first time a sandbox uses it, and
caches the result by the reference's text.
A library that honors `spec.image.reference` (`Capabilities::spec.image_reference`) registers a
reference with `register_reference`, or as the setup's default image when a backend opens: it
converts the reference into a local disk image with an external tool, once, and keeps the
result by the reference's text. A sandbox boots only a registered reference.
The tool comes from the runtime bundle (`bin/direct-images[.exe]`), so a library given explicit
runtime files does not materialize references. It does not check a tag again, so a moving tag
such as `latest` keeps naming the content that was first pulled: use a version tag or a digest
Expand Down Expand Up @@ -212,9 +213,10 @@ not search the user's Docker configuration or other credential files.
The host selects credentials only for the requested registry, and rereads
the file on each actual pull so it can be replaced to rotate credentials.
An unrelated registry is pulled anonymously; a malformed or unreadable
selected file fails the pull. Local and cached images do not read it. The
existing state root remains the cache's trust boundary; credentials do not
restrict access to an image already cached there.
selected file fails the pull. Local images and registered references do not
read it. The existing state root remains the trust boundary of registered
images; credentials do not restrict access to an image already registered
there.

The native host passes credentials through the image tool's existing
password/token command-line options. They are visible to users who can
Expand All @@ -238,20 +240,27 @@ would expose it.
Hosts, guest sessions, and boot-console capture stay in the library for the life of the
process; dropping a backend leaves its sandboxes running.
- **Images.** The library registers images under the state root by their content ID,
`sha256:<hex>` (`ImageId`). Registration hashes an image once; `ImageDigest::Expect`
additionally requires a digest, and `ImageDigest::Trusted` records a digest that the caller's
own policy verified, without reading the file. A later registration, in any process, reuses
one that the file still matches without reading it. Provision records the image ID and the
runtime digests. Start compares each file's seal (volume, file ID, length, and last-write
time, plus the change time on Linux) with the one taken when it was hashed, instead of hashing
again, and fails with `backend_unavailable` if anything changed, so a start costs about the
guest's boot time. A seal detects replacement or modification, not a writer that deliberately
restores timestamps. On Windows, start also keeps writers out of the files until OpenVMM has
opened them. Register a changed image again with `register_image`: unchanged content keeps its
ID, so sandboxes that use it start again. `images` lists the registrations and whether each
file still matches, `verify_image` hashes one again, and `unregister_image` refuses while a
provisioned sandbox uses the image. `Diagnostics::content_verification` hashes the image and
runtime files again before every start, as a diagnostic.
`sha256:<hex>` (`ImageId`), the SHA-256 of the registered file as the caller gave it.
Registration is the one operation that reads an image: it hashes the image once, unless
`ImageDigest::Trusted` records a digest that the caller's own policy verified, and
`ImageDigest::Expect` additionally requires a digest. It then stages the image as a read-only
copy of the library's own, in raw form (a fixed VHD loses its footer but keeps its ID), which
`RegisteredImage::path` names and sandboxes attach, so later changes to the registered file do
not reach them. A later registration, in any process, reuses one that the file still matches
without reading the file. Provision takes only registered images: by ID, by the path that was
registered, or by the reference. A path whose file changed since its registration fails until
it is registered again, and provision never pulls, copies, or hashes an image; it records the
image ID and the runtime digests. Start compares the seal of each runtime file and of the
staged copy (volume, file ID, length, and last-write time, plus the change time on Linux) with
the one taken when it was hashed or staged, instead of hashing again, hands OpenVMM the copy
that it checked, and fails with `backend_unavailable` if anything changed, so a start costs
about the guest's boot time. A seal detects replacement or modification, not a writer that
deliberately restores timestamps. On Windows, start also keeps writers out of the files until
OpenVMM has opened them. Register an image again with `register_image` to stage it anew:
unchanged content keeps its ID, so sandboxes that use it start again. `images` lists the
registrations and whether each staged copy still matches, `verify_image` hashes one again, as a
diagnostic, and `unregister_image` refuses while a provisioned sandbox uses the image, and
otherwise removes its staged copy.
- **The guest.** The image is attached read-only; the edge guest validates its GPT and p2+ ext4
layers and keeps its writable layer in RAM, so it needs no scratch disk. Start sends OpenVMM a
fresh 32-byte capability through standard input, checks the process that serves the control
Expand Down
5 changes: 1 addition & 4 deletions aci_edge_sandboxes/examples/agent_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,7 @@ fn run() -> Result<(), String> {
.with_image(image)
.with_image_digest(image_digest),
)
.with_diagnostics(Diagnostics {
guest_debug,
..Diagnostics::default()
});
.with_diagnostics(Diagnostics { guest_debug });
if let Some(hypervisor) = hypervisor {
setup = setup.with_hypervisor(hypervisor);
}
Expand Down
2 changes: 1 addition & 1 deletion aci_edge_sandboxes/model/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub struct SpecCapabilities {
pub image_path: bool,
/// Honors `image.digest`: an image registered with the backend.
pub image_digest: bool,
/// Honors `image.reference`: a container image reference.
/// Honors `image.reference`: a container image reference, which the backend also registers.
pub image_reference: bool,
/// Honors `resources.vcpus`.
pub vcpus: bool,
Expand Down
19 changes: 11 additions & 8 deletions aci_edge_sandboxes/model/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::{Error, Result};
use crate::hex;

/// Content identity of a registered guest image: the SHA-256 digest of the image file.
/// Content identity of a registered guest image: the SHA-256 digest of the image file as it was
/// registered, such as a fixed VHD with its footer, which a raw copy that a host stages of it keeps.
///
/// An ID displays and parses as `sha256:` followed by 64 lowercase hexadecimal digits.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
Expand Down Expand Up @@ -85,12 +86,12 @@ impl<'de> Deserialize<'de> for ImageId {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ImageDigest {
/// Hashes the file.
/// Hashes the registered file.
#[default]
Compute,
/// Hashes the file and requires this SHA-256 digest.
/// Hashes the registered file and requires this SHA-256 digest.
Expect(#[serde(with = "hex::sha256")] [u8; 32]),
/// Records this SHA-256 digest without reading the file.
/// Records this SHA-256 digest of the registered file without hashing it.
///
/// Use it only for content that the caller's own policy has verified and protects from
/// writers, for example a file that an installer hashed before making it read-only.
Expand All @@ -109,13 +110,15 @@ impl ImageDigest {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[non_exhaustive]
pub struct RegisteredImage {
/// Content identity.
/// Content identity: the SHA-256 digest of the file that was registered.
pub id: ImageId,
/// Absolute path that sandboxes attach the image from.
/// Absolute path of the file that sandboxes attach: the host's own staged copy of the image,
/// for a host that stages images. A raw copy lacks the container format of the registered
/// file, such as a fixed VHD's footer, but keeps its ID.
pub path: PathBuf,
/// Length in bytes at registration.
/// Length in bytes of the file at [`path`](Self::path).
pub length: u64,
/// Whether the caller supplied the digest instead of the host hashing the file.
/// Whether the caller supplied the digest instead of the host hashing the image.
pub trusted: bool,
/// Whether the file still matches its registration. A sandbox whose image changed does not
/// start until the image is registered again.
Expand Down
17 changes: 9 additions & 8 deletions aci_edge_sandboxes/model/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,10 @@ impl ImageSettings {

/// How a host that pulls registry references materializes them.
///
/// Such a host converts an [`ImageSource::Reference`] into a local disk image with an external
/// tool the first time a sandbox uses it, and caches the result by the reference's text: later
/// sandboxes with the same reference boot the cached image, and the host does not check a tag
/// Such a host registers an [`ImageSource::Reference`] when the caller registers it, or when the
/// host opens with it as the default image: it converts the reference into a local disk image
/// with an external tool, once, and keeps the result by the reference's text. Sandboxes that name
/// the reference boot that image, provisioning never pulls, and the host does not check a tag
/// again. A moving tag such as `latest` therefore keeps naming the content that was first pulled;
/// use a version tag or a digest (`repository@sha256:…`) to control what a sandbox boots.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -474,7 +475,8 @@ pub struct ReferenceSettings {
#[serde(default = "default_pull_timeout_ms")]
pub pull_timeout_ms: u64,
/// Host-side Docker-compatible JSON credentials file. `None` keeps anonymous pulls.
/// The host reads it only when it must pull a reference, not for local or cached images.
/// The host reads it only when it must pull a reference, not for local images or registered
/// references.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_file: Option<PathBuf>,
}
Expand Down Expand Up @@ -589,16 +591,15 @@ impl ExecSettings {
}

/// Diagnostic switches.
///
/// None of them makes an operation hash an image or a runtime file: `verify_image` of the agent
/// backend hashes an image again, as a diagnostic of its own.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Diagnostics {
/// Boots guests with verbose kernel and agent logging.
#[serde(default)]
pub guest_debug: bool,
/// Hashes every runtime file and image again before each start, instead of trusting their
/// recorded identity.
#[serde(default)]
pub content_verification: bool,
}

#[cfg(test)]
Expand Down
63 changes: 40 additions & 23 deletions aci_edge_sandboxes/model/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! guest's resources and network identity, and forwarded ports. Every field is optional; unset
//! fields take the backend's defaults, and nothing here is specific to one VMM.

use std::path::PathBuf;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -110,17 +110,22 @@ impl SandboxSpec {
///
/// It serializes as `{"reference": "registry/repository:tag"}`, `{"digest": "sha256:…"}`, or
/// `{"path": "…"}`.
///
/// A backend that registers images provisions only images registered with it: a path or a
/// reference that it has not registered fails, and provisioning never pulls, copies, or hashes an
/// image. The setup's default image is registered when the backend opens.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[non_exhaustive]
pub enum ImageSource {
/// A container image reference, such as `registry/repository:tag` or
/// `registry/repository@sha256:…`. A host that pulls references caches each one by its text
/// and does not check a tag again; see [`ReferenceSettings`](crate::ReferenceSettings).
/// `registry/repository@sha256:…`. A host that pulls references registers each one by its
/// text, pulling it once, and does not check a tag again; see
/// [`ReferenceSettings`](crate::ReferenceSettings).
Reference(String),
/// An image already registered with the backend, by content.
Digest(ImageId),
/// A local GPT disk image, by absolute path.
/// A local GPT disk image, by the absolute path from which it was registered.
Path(PathBuf),
}

Expand All @@ -130,30 +135,42 @@ impl ImageSource {
pub fn validate(&self, field: &str) -> Result<()> {
match self {
Self::Reference(reference) => {
if reference.is_empty()
|| reference
.chars()
.any(|character| character.is_whitespace() || character.is_control())
{
return Err(Error::malformed_request(format!(
"{field}.reference must be a nonempty image reference without whitespace \
or control characters"
)));
}
}
Self::Digest(_) => {}
Self::Path(path) => {
if !path.is_absolute() || path.to_str().is_none_or(|value| value.contains('\0')) {
return Err(Error::malformed_request(format!(
"{field}.path must be an absolute valid UTF-8 path without NUL characters"
)));
}
validate_reference(reference, &format!("{field}.reference"))
}
Self::Digest(_) => Ok(()),
Self::Path(path) => validate_path(path, &format!("{field}.path")),
}
Ok(())
}
}

/// Checks an image reference, which must be nonempty and hold no whitespace or control
/// characters, reporting [`ErrorCode::MalformedRequest`](crate::ErrorCode::MalformedRequest) for
/// `field`.
pub(crate) fn validate_reference(reference: &str, field: &str) -> Result<()> {
if reference.is_empty()
|| reference
.chars()
.any(|character| character.is_whitespace() || character.is_control())
{
return Err(Error::malformed_request(format!(
"{field} must be a nonempty image reference without whitespace or control characters"
)));
}
Ok(())
}

/// Checks the path of a local image, which must be absolute and valid UTF-8 without NUL
/// characters, reporting [`ErrorCode::MalformedRequest`](crate::ErrorCode::MalformedRequest) for
/// `field`.
pub(crate) fn validate_path(path: &Path, field: &str) -> Result<()> {
if !path.is_absolute() || path.to_str().is_none_or(|value| value.contains('\0')) {
return Err(Error::malformed_request(format!(
"{field} must be an absolute valid UTF-8 path without NUL characters"
)));
}
Ok(())
}

/// Guest resources.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand Down
Loading
Loading