2026-07-15 14:59:43 +09:00
|
|
|
use std::ffi::{OsStr, OsString};
|
|
|
|
|
use std::fmt;
|
2026-07-15 15:16:25 +09:00
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::{Read, Write};
|
|
|
|
|
use std::os::fd::{AsFd, OwnedFd};
|
2026-07-15 14:59:43 +09:00
|
|
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
|
|
|
|
use std::path::{Component, Path, PathBuf};
|
2026-07-22 03:08:27 +09:00
|
|
|
use std::sync::LazyLock;
|
2026-07-15 14:59:43 +09:00
|
|
|
|
2026-07-16 15:20:13 +09:00
|
|
|
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration, SandboxLayoutV1};
|
|
|
|
|
use lazaret_core::identity::{ConfigurationFingerprint, ContainerId, DomainId, NamespaceIdentity};
|
2026-07-15 14:59:43 +09:00
|
|
|
use lazaret_core::records::VolumeAccess;
|
2026-07-19 04:36:44 +09:00
|
|
|
pub use lazaret_sandbox_contract::{
|
|
|
|
|
CONTROL_ROOT_DESTINATION, CREDENTIALS_DESTINATION, DEV_DESTINATION,
|
2026-07-19 05:42:17 +09:00
|
|
|
ESCALATION_CLIENT_DESTINATION, HOSTNAME_DESTINATION, HOSTS_DESTINATION,
|
|
|
|
|
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, PROC_DESTINATION,
|
2026-07-19 22:39:31 +09:00
|
|
|
READINESS_DESTINATION, RESOLV_CONF_DESTINATION, RUNTIME_FILE_DESTINATIONS,
|
|
|
|
|
SANDBOX_INIT_DESTINATION, SYS_DESTINATION, TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
|
2026-07-19 04:36:44 +09:00
|
|
|
};
|
2026-07-15 14:59:43 +09:00
|
|
|
use oci_spec::runtime::{
|
2026-07-22 03:08:27 +09:00
|
|
|
Capabilities, Capability, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder,
|
2026-07-19 19:51:47 +09:00
|
|
|
LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder, Spec, SpecBuilder,
|
|
|
|
|
UserBuilder,
|
2026-07-15 14:59:43 +09:00
|
|
|
};
|
2026-07-16 15:20:13 +09:00
|
|
|
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, StatxFlags, openat2, statx};
|
|
|
|
|
use sha2::{Digest, Sha256};
|
2026-07-15 14:59:43 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
use mounts::{bind_mount, control_mounts, standard_mounts};
|
|
|
|
|
use runtime_sources::{
|
|
|
|
|
build_linux, build_mounts, build_process, build_runsc_command, descriptor_identity,
|
|
|
|
|
handoff_runtime_sources, open_absolute_directory, open_runtime_source, require_exact_bytes,
|
|
|
|
|
runtime_paths,
|
|
|
|
|
};
|
|
|
|
|
use validation::{
|
|
|
|
|
changed_authority_at, changed_configuration_authority, confined_resolve_flags, hex_lower,
|
|
|
|
|
host_path, oci_error, open_or_create_staging_directory, read_descriptor,
|
|
|
|
|
require_network_namespace, require_staged_file, stage_exact_file, staging_error,
|
|
|
|
|
validate_absolute_normalized, validate_compiled_terminfo, validate_sandbox_executable,
|
|
|
|
|
validate_terminal_name,
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-15 14:59:43 +09:00
|
|
|
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
|
|
|
|
|
|
|
|
|
const OCI_VERSION: &str = "1.2.1";
|
2026-07-15 15:16:25 +09:00
|
|
|
const MAX_TERMINFO_BYTES: usize = 1_048_576;
|
2026-07-15 14:59:43 +09:00
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum SandboxBuildErrorKind {
|
|
|
|
|
InvalidProcess,
|
|
|
|
|
InvalidPath,
|
|
|
|
|
InvalidTerminal,
|
|
|
|
|
ChangedAuthority,
|
|
|
|
|
NetworkNamespaceMismatch,
|
|
|
|
|
OciSpecification,
|
|
|
|
|
JsonEncoding,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct SandboxBuildError {
|
|
|
|
|
kind: SandboxBuildErrorKind,
|
|
|
|
|
detail: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SandboxBuildError {
|
|
|
|
|
fn new(kind: SandboxBuildErrorKind, detail: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
kind,
|
|
|
|
|
detail: detail.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn kind(&self) -> SandboxBuildErrorKind {
|
|
|
|
|
self.kind
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for SandboxBuildError {
|
|
|
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
write!(formatter, "{}: {}", error_label(self.kind), self.detail)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for SandboxBuildError {}
|
|
|
|
|
|
|
|
|
|
const fn error_label(kind: SandboxBuildErrorKind) -> &'static str {
|
|
|
|
|
match kind {
|
|
|
|
|
SandboxBuildErrorKind::InvalidProcess => "invalid root process",
|
|
|
|
|
SandboxBuildErrorKind::InvalidPath => "invalid sandbox path",
|
|
|
|
|
SandboxBuildErrorKind::InvalidTerminal => "invalid host terminal",
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority => "changed host authority",
|
|
|
|
|
SandboxBuildErrorKind::NetworkNamespaceMismatch => "network namespace mismatch",
|
|
|
|
|
SandboxBuildErrorKind::OciSpecification => "invalid OCI specification",
|
|
|
|
|
SandboxBuildErrorKind::JsonEncoding => "OCI JSON encoding failed",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct RootProcess {
|
|
|
|
|
arguments: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RootProcess {
|
|
|
|
|
/// Creates a direct argument vector for the terminal-neutral root process.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// The program must be an absolute normalized sandbox path and no argument may contain NUL.
|
|
|
|
|
pub fn new(
|
|
|
|
|
program: impl Into<String>,
|
|
|
|
|
arguments: impl IntoIterator<Item = String>,
|
|
|
|
|
) -> Result<Self, SandboxBuildError> {
|
|
|
|
|
let program = program.into();
|
|
|
|
|
validate_sandbox_executable(&program)?;
|
|
|
|
|
let mut complete = vec![program];
|
|
|
|
|
for argument in arguments {
|
|
|
|
|
if argument.as_bytes().contains(&0) {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidProcess,
|
|
|
|
|
"argument contains NUL",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
complete.push(argument);
|
|
|
|
|
}
|
|
|
|
|
Ok(Self {
|
|
|
|
|
arguments: complete,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn arguments(&self) -> &[String] {
|
|
|
|
|
&self.arguments
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
struct FileIdentity {
|
|
|
|
|
device: u64,
|
|
|
|
|
inode: u64,
|
2026-07-16 15:20:13 +09:00
|
|
|
mount_id: u64,
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FileIdentity {
|
2026-07-15 15:16:25 +09:00
|
|
|
fn from_descriptor(descriptor: &OwnedFd) -> Result<Self, SandboxBuildError> {
|
|
|
|
|
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
2026-07-16 15:20:13 +09:00
|
|
|
let extended = statx(descriptor, "", AtFlags::EMPTY_PATH, StatxFlags::MNT_ID)
|
|
|
|
|
.map_err(staging_error)?;
|
|
|
|
|
if !StatxFlags::from_bits_retain(extended.stx_mask).contains(StatxFlags::MNT_ID) {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"host filesystem does not report mount identity",
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-07-15 15:16:25 +09:00
|
|
|
if !FileType::from_raw_mode(stat.st_mode).is_file() {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"staged terminfo artifact is not a regular file",
|
|
|
|
|
));
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
2026-07-15 15:16:25 +09:00
|
|
|
Ok(Self {
|
|
|
|
|
device: stat.st_dev,
|
|
|
|
|
inode: stat.st_ino,
|
2026-07-16 15:20:13 +09:00
|
|
|
mount_id: extended.stx_mnt_id,
|
2026-07-15 15:16:25 +09:00
|
|
|
})
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 15:20:13 +09:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
enum HandoffPathType {
|
|
|
|
|
Directory,
|
|
|
|
|
RegularFile,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
struct DerivedHandoffAuthority {
|
|
|
|
|
relative_path: PathBuf,
|
|
|
|
|
expected_type: HandoffPathType,
|
|
|
|
|
identity: FileIdentity,
|
|
|
|
|
expected_bytes: Option<Vec<u8>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
struct RuntimeHandoffSource {
|
|
|
|
|
relative_path: PathBuf,
|
|
|
|
|
expected_type: HandoffPathType,
|
|
|
|
|
build_identity: Option<FileIdentity>,
|
|
|
|
|
expected_bytes: Option<Vec<u8>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
struct PathAuthorityIdentity {
|
|
|
|
|
device: u64,
|
|
|
|
|
inode: u64,
|
|
|
|
|
mount_id: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PathAuthorityIdentity {
|
|
|
|
|
fn from_plan(plan: &ConfinedPathPlan) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
device: plan.target_device().get(),
|
|
|
|
|
inode: plan.target_inode().get(),
|
|
|
|
|
mount_id: plan.target_mount_id().get(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
struct ConfigurationAuthorityIdentity {
|
|
|
|
|
workspace: PathAuthorityIdentity,
|
|
|
|
|
runtime_domain: PathAuthorityIdentity,
|
|
|
|
|
volumes: Vec<PathAuthorityIdentity>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConfigurationAuthorityIdentity {
|
|
|
|
|
fn capture(configuration: &ResolvedDomainConfiguration) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
workspace: PathAuthorityIdentity::from_plan(configuration.workspace()),
|
|
|
|
|
runtime_domain: PathAuthorityIdentity::from_plan(configuration.runtime_domain_root()),
|
|
|
|
|
volumes: configuration
|
|
|
|
|
.volumes()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|volume| PathAuthorityIdentity::from_plan(volume.source_plan()))
|
|
|
|
|
.collect(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct SandboxHandoffAuthority<'configuration> {
|
|
|
|
|
configuration: &'configuration ResolvedDomainConfiguration,
|
|
|
|
|
domain_id: DomainId,
|
|
|
|
|
configuration_fingerprint: ConfigurationFingerprint,
|
|
|
|
|
configuration_authority: ConfigurationAuthorityIdentity,
|
|
|
|
|
command: RunscCommand,
|
|
|
|
|
derived: Vec<DerivedHandoffAuthority>,
|
2026-07-17 03:58:27 +09:00
|
|
|
prepared_root: PathBuf,
|
|
|
|
|
prepared_root_identity: FileIdentity,
|
2026-07-16 15:20:13 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SandboxHandoffAuthority<'_> {
|
|
|
|
|
/// Revalidates every pathname source immediately before attached runsc is released.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses changed workspace, volume, Runtime Domain, bundle, rootfs, control, or terminal
|
|
|
|
|
/// authority.
|
|
|
|
|
pub fn revalidate(&self) -> Result<(), SandboxBuildError> {
|
|
|
|
|
if self.configuration.domain_id() != self.domain_id
|
|
|
|
|
|| self.configuration.configuration_fingerprint() != self.configuration_fingerprint
|
|
|
|
|
|| ConfigurationAuthorityIdentity::capture(self.configuration)
|
|
|
|
|
!= self.configuration_authority
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"sandbox handoff configuration does not match its bundle",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
self.configuration
|
|
|
|
|
.workspace()
|
|
|
|
|
.open_authority()
|
2026-07-17 03:58:27 +09:00
|
|
|
.map_err(|error| changed_authority_at("workspace", error))?;
|
2026-07-16 15:20:13 +09:00
|
|
|
for volume in self.configuration.volumes() {
|
|
|
|
|
volume
|
|
|
|
|
.open_source()
|
2026-07-17 03:58:27 +09:00
|
|
|
.map_err(|error| changed_authority_at("volume", error))?;
|
|
|
|
|
}
|
|
|
|
|
let prepared_root = open_absolute_directory(&self.prepared_root)?;
|
|
|
|
|
if descriptor_identity(&prepared_root).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
format!("prepared root identity: {error}"),
|
|
|
|
|
)
|
|
|
|
|
})? != self.prepared_root_identity
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"prepared root changed identity after bundle construction",
|
|
|
|
|
));
|
2026-07-16 15:20:13 +09:00
|
|
|
}
|
|
|
|
|
let runtime_root = self
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.open_authority()
|
2026-07-17 03:58:27 +09:00
|
|
|
.map_err(|error| changed_authority_at("Runtime Domain", error))?;
|
2026-07-16 15:20:13 +09:00
|
|
|
for authority in &self.derived {
|
|
|
|
|
let descriptor = open_runtime_source(
|
|
|
|
|
&runtime_root,
|
|
|
|
|
&authority.relative_path,
|
|
|
|
|
authority.expected_type,
|
|
|
|
|
authority.expected_bytes.is_some(),
|
|
|
|
|
)?;
|
2026-07-17 03:58:27 +09:00
|
|
|
if descriptor_identity(&descriptor).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
format!(
|
|
|
|
|
"runtime handoff source {} identity: {error}",
|
|
|
|
|
authority.relative_path.display()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
})? != authority.identity
|
|
|
|
|
{
|
2026-07-16 15:20:13 +09:00
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"runtime handoff source changed identity",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if let Some(expected) = &authority.expected_bytes {
|
|
|
|
|
require_exact_bytes(descriptor, expected)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn domain_id(&self) -> DomainId {
|
|
|
|
|
self.domain_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn matches_command(&self, command: &RunscCommand) -> bool {
|
|
|
|
|
self.command == *command
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 14:59:43 +09:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct HostTerminal {
|
|
|
|
|
term: String,
|
2026-07-15 15:16:25 +09:00
|
|
|
compiled_entry: Vec<u8>,
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HostTerminal {
|
2026-07-15 15:16:25 +09:00
|
|
|
/// Accepts one bounded compiled terminfo entry without accepting a host path.
|
2026-07-15 14:59:43 +09:00
|
|
|
///
|
|
|
|
|
/// # Errors
|
2026-07-15 15:16:25 +09:00
|
|
|
/// Invalid names, colors, terminfo headers, aliases, and oversized entries are refused.
|
|
|
|
|
pub fn from_compiled_entry(
|
2026-07-15 14:59:43 +09:00
|
|
|
term: impl Into<String>,
|
2026-07-19 22:39:31 +09:00
|
|
|
color_term: Option<&str>,
|
2026-07-15 15:16:25 +09:00
|
|
|
compiled_entry: Vec<u8>,
|
2026-07-15 14:59:43 +09:00
|
|
|
) -> Result<Self, SandboxBuildError> {
|
|
|
|
|
let term = term.into();
|
|
|
|
|
validate_terminal_name(&term)?;
|
2026-07-19 22:39:31 +09:00
|
|
|
if color_term.is_some_and(|value| {
|
2026-07-15 14:59:43 +09:00
|
|
|
value.is_empty() || value.bytes().any(|byte| byte < 0x20 || byte == 0x7f)
|
|
|
|
|
}) {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidTerminal,
|
|
|
|
|
"COLORTERM contains control bytes or is empty",
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-07-15 15:16:25 +09:00
|
|
|
validate_compiled_terminfo(&term, &compiled_entry)?;
|
2026-07-15 14:59:43 +09:00
|
|
|
Ok(Self {
|
|
|
|
|
term,
|
2026-07-15 15:16:25 +09:00
|
|
|
compiled_entry,
|
2026-07-15 14:59:43 +09:00
|
|
|
})
|
|
|
|
|
}
|
2026-07-15 15:16:25 +09:00
|
|
|
}
|
2026-07-15 14:59:43 +09:00
|
|
|
|
2026-07-15 15:16:25 +09:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct StagedHostTerminal {
|
|
|
|
|
term: String,
|
|
|
|
|
compiled_entry: Vec<u8>,
|
|
|
|
|
relative_source: PathBuf,
|
|
|
|
|
identity: FileIdentity,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StagedHostTerminal {
|
|
|
|
|
fn revalidate(
|
|
|
|
|
&self,
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
) -> Result<(), SandboxBuildError> {
|
|
|
|
|
let root = configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.open_authority()
|
|
|
|
|
.map_err(changed_configuration_authority)?;
|
|
|
|
|
let descriptor = openat2(
|
|
|
|
|
root,
|
|
|
|
|
&self.relative_source,
|
|
|
|
|
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
|
|
|
|
Mode::empty(),
|
|
|
|
|
confined_resolve_flags(),
|
|
|
|
|
)
|
|
|
|
|
.map_err(staging_error)?;
|
|
|
|
|
require_staged_file(&descriptor)?;
|
|
|
|
|
if FileIdentity::from_descriptor(&descriptor)? != self.identity
|
|
|
|
|
|| read_descriptor(descriptor)? != self.compiled_entry
|
|
|
|
|
{
|
2026-07-15 14:59:43 +09:00
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
2026-07-15 15:16:25 +09:00
|
|
|
"staged terminfo artifact changed identity or bytes",
|
2026-07-15 14:59:43 +09:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn destination(&self) -> PathBuf {
|
|
|
|
|
let bucket = char::from(self.term.as_bytes()[0]).to_string();
|
|
|
|
|
Path::new(TERMINFO_ROOT_DESTINATION)
|
|
|
|
|
.join(bucket)
|
|
|
|
|
.join(&self.term)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 15:16:25 +09:00
|
|
|
/// Copies validated terminfo bytes into the generation's fixed control authority.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses changed Runtime Domain authority, unsafe staging objects, or byte mismatches.
|
|
|
|
|
pub fn stage_host_terminal(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
container_id: ContainerId,
|
|
|
|
|
terminal: HostTerminal,
|
|
|
|
|
) -> Result<StagedHostTerminal, SandboxBuildError> {
|
|
|
|
|
let container_name = hex_lower(container_id.as_bytes());
|
|
|
|
|
let bucket = char::from(terminal.term.as_bytes()[0]).to_string();
|
|
|
|
|
let components = [
|
|
|
|
|
"sandbox-control",
|
|
|
|
|
container_name.as_str(),
|
|
|
|
|
"terminfo",
|
|
|
|
|
bucket.as_str(),
|
|
|
|
|
];
|
|
|
|
|
let mut directory = configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.open_authority()
|
|
|
|
|
.map_err(changed_configuration_authority)?;
|
|
|
|
|
for component in components {
|
|
|
|
|
directory = open_or_create_staging_directory(directory, component)?;
|
|
|
|
|
}
|
|
|
|
|
let descriptor = stage_exact_file(&directory, &terminal.term, &terminal.compiled_entry)?;
|
|
|
|
|
let identity = FileIdentity::from_descriptor(&descriptor)?;
|
|
|
|
|
if read_descriptor(descriptor)? != terminal.compiled_entry {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"staged terminfo bytes differ from the selected entry",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(StagedHostTerminal {
|
|
|
|
|
relative_source: Path::new("sandbox-control")
|
|
|
|
|
.join(container_name)
|
|
|
|
|
.join("terminfo")
|
|
|
|
|
.join(bucket)
|
|
|
|
|
.join(&terminal.term),
|
|
|
|
|
term: terminal.term,
|
|
|
|
|
compiled_entry: terminal.compiled_entry,
|
|
|
|
|
identity,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 14:59:43 +09:00
|
|
|
pub struct SandboxBuildRequest<'a> {
|
|
|
|
|
pub configuration: &'a ResolvedDomainConfiguration,
|
|
|
|
|
pub container_id: ContainerId,
|
|
|
|
|
pub expected_network_namespace: NamespaceIdentity,
|
|
|
|
|
pub root_process: RootProcess,
|
2026-07-15 15:16:25 +09:00
|
|
|
pub terminal: Option<StagedHostTerminal>,
|
2026-07-17 03:58:27 +09:00
|
|
|
pub prepared_root: PathBuf,
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct RunscCommand {
|
|
|
|
|
program: OsString,
|
|
|
|
|
arguments: Vec<OsString>,
|
2026-07-16 15:20:13 +09:00
|
|
|
bundle_authorization: [u8; 32],
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RunscCommand {
|
2026-07-16 15:20:13 +09:00
|
|
|
pub(crate) fn new(
|
|
|
|
|
program: impl Into<OsString>,
|
|
|
|
|
arguments: impl IntoIterator<Item = OsString>,
|
|
|
|
|
bundle_authorization: [u8; 32],
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
program: program.into(),
|
|
|
|
|
arguments: arguments.into_iter().collect(),
|
|
|
|
|
bundle_authorization,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 14:59:43 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn program(&self) -> &OsStr {
|
|
|
|
|
&self.program
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn arguments(&self) -> &[OsString] {
|
|
|
|
|
&self.arguments
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct SandboxBundle {
|
|
|
|
|
specification: Spec,
|
|
|
|
|
specification_json: Vec<u8>,
|
|
|
|
|
command: RunscCommand,
|
2026-07-16 15:20:13 +09:00
|
|
|
domain_id: DomainId,
|
|
|
|
|
configuration_fingerprint: ConfigurationFingerprint,
|
|
|
|
|
configuration_authority: ConfigurationAuthorityIdentity,
|
|
|
|
|
runtime_sources: Vec<RuntimeHandoffSource>,
|
2026-07-17 03:58:27 +09:00
|
|
|
prepared_root: PathBuf,
|
|
|
|
|
prepared_root_identity: FileIdentity,
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SandboxBundle {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn specification(&self) -> &Spec {
|
|
|
|
|
&self.specification
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn specification_json(&self) -> &[u8] {
|
|
|
|
|
&self.specification_json
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn command(&self) -> &RunscCommand {
|
|
|
|
|
&self.command
|
|
|
|
|
}
|
2026-07-16 15:20:13 +09:00
|
|
|
|
|
|
|
|
/// Captures the exact staged sources that must survive until attached runsc release.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses absent, changed, symlinked, or incorrectly typed handoff sources.
|
|
|
|
|
pub fn capture_handoff_authority<'configuration>(
|
|
|
|
|
&self,
|
|
|
|
|
configuration: &'configuration ResolvedDomainConfiguration,
|
|
|
|
|
) -> Result<SandboxHandoffAuthority<'configuration>, SandboxBuildError> {
|
|
|
|
|
if configuration.domain_id() != self.domain_id
|
|
|
|
|
|| configuration.configuration_fingerprint() != self.configuration_fingerprint
|
|
|
|
|
|| ConfigurationAuthorityIdentity::capture(configuration)
|
|
|
|
|
!= self.configuration_authority
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"sandbox handoff configuration does not match its bundle",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
configuration
|
|
|
|
|
.workspace()
|
|
|
|
|
.open_authority()
|
2026-07-17 03:58:27 +09:00
|
|
|
.map_err(|error| changed_authority_at("workspace", error))?;
|
2026-07-16 15:20:13 +09:00
|
|
|
for volume in configuration.volumes() {
|
|
|
|
|
volume
|
|
|
|
|
.open_source()
|
2026-07-17 03:58:27 +09:00
|
|
|
.map_err(|error| changed_authority_at("volume", error))?;
|
2026-07-16 15:20:13 +09:00
|
|
|
}
|
|
|
|
|
let runtime_root = configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.open_authority()
|
2026-07-17 03:58:27 +09:00
|
|
|
.map_err(|error| changed_authority_at("Runtime Domain", error))?;
|
|
|
|
|
let prepared_root = open_absolute_directory(&self.prepared_root)?;
|
|
|
|
|
if descriptor_identity(&prepared_root).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
format!("prepared root identity: {error}"),
|
|
|
|
|
)
|
|
|
|
|
})? != self.prepared_root_identity
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"prepared root changed identity after bundle construction",
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-07-16 15:20:13 +09:00
|
|
|
let derived = self
|
|
|
|
|
.runtime_sources
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|source| {
|
|
|
|
|
let descriptor = open_runtime_source(
|
|
|
|
|
&runtime_root,
|
|
|
|
|
&source.relative_path,
|
|
|
|
|
source.expected_type,
|
|
|
|
|
source.expected_bytes.is_some(),
|
|
|
|
|
)?;
|
2026-07-17 03:58:27 +09:00
|
|
|
let identity = descriptor_identity(&descriptor).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
format!(
|
|
|
|
|
"runtime handoff source {} identity: {error}",
|
|
|
|
|
source.relative_path.display()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
2026-07-16 15:20:13 +09:00
|
|
|
if source
|
|
|
|
|
.build_identity
|
|
|
|
|
.is_some_and(|expected| expected != identity)
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"staged handoff source changed identity after bundle construction",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if let Some(expected) = &source.expected_bytes {
|
|
|
|
|
require_exact_bytes(descriptor, expected)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(DerivedHandoffAuthority {
|
|
|
|
|
relative_path: source.relative_path.clone(),
|
|
|
|
|
expected_type: source.expected_type,
|
|
|
|
|
identity,
|
|
|
|
|
expected_bytes: source.expected_bytes.clone(),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect::<Result<_, SandboxBuildError>>()?;
|
|
|
|
|
Ok(SandboxHandoffAuthority {
|
|
|
|
|
configuration,
|
|
|
|
|
domain_id: self.domain_id,
|
|
|
|
|
configuration_fingerprint: self.configuration_fingerprint,
|
|
|
|
|
configuration_authority: self.configuration_authority.clone(),
|
|
|
|
|
command: self.command.clone(),
|
|
|
|
|
derived,
|
2026-07-17 03:58:27 +09:00
|
|
|
prepared_root: self.prepared_root.clone(),
|
|
|
|
|
prepared_root_identity: self.prepared_root_identity,
|
2026-07-16 15:20:13 +09:00
|
|
|
})
|
|
|
|
|
}
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the complete protected destination roots used by bundle construction.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error if the fixed layout violates the configuration layout contract.
|
2026-07-18 22:35:48 +09:00
|
|
|
pub fn default_sandbox_layout() -> Result<SandboxLayoutV1, SandboxBuildError> {
|
2026-07-19 04:36:44 +09:00
|
|
|
lazaret_sandbox_contract::default_sandbox_layout().map_err(|error| {
|
2026-07-15 14:59:43 +09:00
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidPath,
|
2026-07-18 22:35:48 +09:00
|
|
|
format!("default sandbox layout is invalid: {error}"),
|
2026-07-15 14:59:43 +09:00
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds deterministic OCI bytes and the direct attached-runsc argument vector.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses changed host authority, namespace mismatch, invalid paths, or encoding failures.
|
|
|
|
|
pub fn build_sandbox_bundle(
|
|
|
|
|
request: SandboxBuildRequest<'_>,
|
2026-07-18 02:08:14 +09:00
|
|
|
) -> Result<SandboxBundle, SandboxBuildError> {
|
|
|
|
|
let workspace_source = host_path(request.configuration.workspace().canonical_label())?;
|
|
|
|
|
build_sandbox_bundle_from_source(request, workspace_source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds deterministic OCI bytes using a generation-local routed workspace source.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses an invalid source, changed host authority, namespace mismatch, or encoding failure.
|
|
|
|
|
pub fn build_sandbox_bundle_with_workspace(
|
|
|
|
|
request: SandboxBuildRequest<'_>,
|
|
|
|
|
workspace_source: PathBuf,
|
|
|
|
|
) -> Result<SandboxBundle, SandboxBuildError> {
|
|
|
|
|
build_sandbox_bundle_from_source(request, workspace_source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_sandbox_bundle_from_source(
|
|
|
|
|
request: SandboxBuildRequest<'_>,
|
|
|
|
|
workspace_source: PathBuf,
|
2026-07-15 14:59:43 +09:00
|
|
|
) -> Result<SandboxBundle, SandboxBuildError> {
|
|
|
|
|
require_network_namespace(request.expected_network_namespace)?;
|
|
|
|
|
request
|
|
|
|
|
.configuration
|
|
|
|
|
.workspace()
|
|
|
|
|
.open_authority()
|
|
|
|
|
.map_err(changed_configuration_authority)?;
|
|
|
|
|
for volume in request.configuration.volumes() {
|
|
|
|
|
volume
|
|
|
|
|
.open_source()
|
|
|
|
|
.map_err(changed_configuration_authority)?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(terminal) = &request.terminal {
|
2026-07-15 15:16:25 +09:00
|
|
|
terminal.revalidate(request.configuration)?;
|
2026-07-15 14:59:43 +09:00
|
|
|
}
|
2026-07-17 03:58:27 +09:00
|
|
|
let prepared_root = open_absolute_directory(&request.prepared_root)?;
|
|
|
|
|
let prepared_root_identity = descriptor_identity(&prepared_root)?;
|
2026-07-15 14:59:43 +09:00
|
|
|
|
|
|
|
|
let paths = runtime_paths(request.configuration, request.container_id)?;
|
|
|
|
|
let mounts = build_mounts(
|
|
|
|
|
request.configuration,
|
|
|
|
|
request.terminal.as_ref(),
|
|
|
|
|
&paths.control_sources,
|
2026-07-18 02:08:14 +09:00
|
|
|
workspace_source,
|
2026-07-15 14:59:43 +09:00
|
|
|
)?;
|
|
|
|
|
let process = build_process(
|
|
|
|
|
request.configuration,
|
|
|
|
|
request.root_process,
|
|
|
|
|
request.terminal.as_ref(),
|
|
|
|
|
)?;
|
|
|
|
|
let linux = build_linux()?;
|
|
|
|
|
let root = RootBuilder::default()
|
2026-07-17 03:58:27 +09:00
|
|
|
.path(request.prepared_root.clone())
|
2026-07-15 14:59:43 +09:00
|
|
|
.readonly(false)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)?;
|
|
|
|
|
let specification = SpecBuilder::default()
|
|
|
|
|
.version(OCI_VERSION)
|
|
|
|
|
.root(root)
|
|
|
|
|
.mounts(mounts)
|
|
|
|
|
.process(process)
|
|
|
|
|
.hostname("lazaret")
|
|
|
|
|
.linux(linux)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)?;
|
2026-07-22 03:08:27 +09:00
|
|
|
let specification_json = canonical_specification_json(&specification)?;
|
2026-07-16 15:20:13 +09:00
|
|
|
let bundle_authorization = Sha256::digest(&specification_json).into();
|
|
|
|
|
let command = build_runsc_command(&paths, bundle_authorization);
|
|
|
|
|
let runtime_sources =
|
|
|
|
|
handoff_runtime_sources(&paths, request.terminal.as_ref(), &specification_json);
|
2026-07-15 14:59:43 +09:00
|
|
|
Ok(SandboxBundle {
|
|
|
|
|
specification,
|
|
|
|
|
specification_json,
|
|
|
|
|
command,
|
2026-07-16 15:20:13 +09:00
|
|
|
domain_id: request.configuration.domain_id(),
|
|
|
|
|
configuration_fingerprint: request.configuration.configuration_fingerprint(),
|
|
|
|
|
configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration),
|
|
|
|
|
runtime_sources,
|
2026-07-17 03:58:27 +09:00
|
|
|
prepared_root: request.prepared_root,
|
|
|
|
|
prepared_root_identity,
|
2026-07-15 14:59:43 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 03:08:27 +09:00
|
|
|
fn canonical_specification_json(specification: &Spec) -> Result<Vec<u8>, SandboxBuildError> {
|
|
|
|
|
let mut value = serde_json::to_value(specification).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())
|
|
|
|
|
})?;
|
|
|
|
|
let capabilities = value
|
|
|
|
|
.pointer_mut("/process/capabilities")
|
|
|
|
|
.and_then(serde_json::Value::as_object_mut)
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::JsonEncoding,
|
|
|
|
|
"OCI process capabilities are missing",
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
for set in capabilities.values_mut() {
|
|
|
|
|
let entries = set.as_array_mut().ok_or_else(|| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::JsonEncoding,
|
|
|
|
|
"OCI process capability set is not an array",
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
entries.sort_by(|left, right| left.as_str().cmp(&right.as_str()));
|
|
|
|
|
}
|
|
|
|
|
serde_json::to_vec(&value).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
mod mounts;
|
|
|
|
|
mod runtime_sources;
|
|
|
|
|
mod validation;
|