2026-07-15 14:59:43 +09:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
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};
|
|
|
|
|
|
|
|
|
|
use lazaret_core::configuration::{ResolvedDomainConfiguration, SandboxLayoutV1};
|
|
|
|
|
use lazaret_core::identity::{ContainerId, NamespaceIdentity};
|
|
|
|
|
use lazaret_core::records::VolumeAccess;
|
|
|
|
|
use oci_spec::runtime::{
|
|
|
|
|
Capabilities, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxIdMappingBuilder,
|
|
|
|
|
LinuxNamespaceBuilder, LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder,
|
|
|
|
|
Spec, SpecBuilder, UserBuilder,
|
|
|
|
|
};
|
2026-07-15 15:16:25 +09:00
|
|
|
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, openat2};
|
2026-07-15 14:59:43 +09:00
|
|
|
|
|
|
|
|
pub const WORKSPACE_DESTINATION: &str = "/workspace";
|
|
|
|
|
pub const CONTROL_ROOT_DESTINATION: &str = "/run/lazaret";
|
|
|
|
|
pub const CREDENTIALS_DESTINATION: &str = "/run/lazaret/credentials";
|
|
|
|
|
pub const MAILBOX_REQUESTS_DESTINATION: &str = "/run/lazaret/mailbox/requests";
|
|
|
|
|
pub const MAILBOX_RESULTS_DESTINATION: &str = "/run/lazaret/mailbox/results";
|
|
|
|
|
pub const ESCALATION_CLIENT_DESTINATION: &str = "/run/lazaret/bin/lazaret-escalate";
|
|
|
|
|
pub const TERMINFO_ROOT_DESTINATION: &str = "/run/lazaret/terminfo";
|
|
|
|
|
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
|
|
|
|
|
|
|
|
|
const OCI_VERSION: &str = "1.2.1";
|
|
|
|
|
const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)?;
|
|
|
|
|
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-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct HostTerminal {
|
|
|
|
|
term: String,
|
|
|
|
|
color_term: Option<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>,
|
|
|
|
|
color_term: Option<String>,
|
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)?;
|
|
|
|
|
if color_term.as_deref().is_some_and(|value| {
|
|
|
|
|
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,
|
|
|
|
|
color_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,
|
|
|
|
|
color_term: Option<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,
|
|
|
|
|
color_term: terminal.color_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-15 14:59:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct RunscCommand {
|
|
|
|
|
program: OsString,
|
|
|
|
|
arguments: Vec<OsString>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RunscCommand {
|
|
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the complete protected destination roots used by bundle construction.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error if the fixed layout violates the configuration layout contract.
|
|
|
|
|
pub fn production_sandbox_layout() -> Result<SandboxLayoutV1, SandboxBuildError> {
|
|
|
|
|
SandboxLayoutV1::from_production_layout([
|
|
|
|
|
WORKSPACE_DESTINATION.to_owned(),
|
|
|
|
|
CONTROL_ROOT_DESTINATION.to_owned(),
|
|
|
|
|
"/proc".to_owned(),
|
|
|
|
|
"/sys".to_owned(),
|
|
|
|
|
"/dev".to_owned(),
|
|
|
|
|
])
|
|
|
|
|
.map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidPath,
|
|
|
|
|
format!("production layout is invalid: {error}"),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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<'_>,
|
|
|
|
|
) -> 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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let paths = runtime_paths(request.configuration, request.container_id)?;
|
|
|
|
|
let mounts = build_mounts(
|
|
|
|
|
request.configuration,
|
|
|
|
|
request.terminal.as_ref(),
|
|
|
|
|
&paths.control_sources,
|
|
|
|
|
)?;
|
|
|
|
|
let process = build_process(
|
|
|
|
|
request.configuration,
|
|
|
|
|
request.root_process,
|
|
|
|
|
request.terminal.as_ref(),
|
|
|
|
|
)?;
|
|
|
|
|
let linux = build_linux()?;
|
|
|
|
|
let command = build_runsc_command(&paths);
|
|
|
|
|
let root = RootBuilder::default()
|
|
|
|
|
.path(paths.rootfs)
|
|
|
|
|
.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)?;
|
|
|
|
|
let specification_json = serde_json::to_vec(&specification).map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())
|
|
|
|
|
})?;
|
|
|
|
|
Ok(SandboxBundle {
|
|
|
|
|
specification,
|
|
|
|
|
specification_json,
|
|
|
|
|
command,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct RuntimePaths {
|
|
|
|
|
container_name: String,
|
|
|
|
|
bundle: PathBuf,
|
|
|
|
|
runsc_root: PathBuf,
|
|
|
|
|
rootfs: PathBuf,
|
|
|
|
|
control_sources: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn runtime_paths(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
container_id: ContainerId,
|
|
|
|
|
) -> Result<RuntimePaths, SandboxBuildError> {
|
|
|
|
|
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
|
|
|
|
let container_name = hex_lower(container_id.as_bytes());
|
|
|
|
|
Ok(RuntimePaths {
|
|
|
|
|
bundle: runtime_domain.join("bundles").join(&container_name),
|
|
|
|
|
runsc_root: runtime_domain.join("runsc"),
|
|
|
|
|
rootfs: runtime_domain.join("state-anchor").join("merged"),
|
2026-07-15 15:16:25 +09:00
|
|
|
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
2026-07-15 14:59:43 +09:00
|
|
|
container_name,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_mounts(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
2026-07-15 15:16:25 +09:00
|
|
|
terminal: Option<&StagedHostTerminal>,
|
2026-07-15 14:59:43 +09:00
|
|
|
control_sources: &Path,
|
|
|
|
|
) -> Result<Vec<Mount>, SandboxBuildError> {
|
|
|
|
|
let mut mounts = standard_mounts()?;
|
|
|
|
|
mounts.push(bind_mount(
|
|
|
|
|
host_path(configuration.workspace().canonical_label())?,
|
|
|
|
|
PathBuf::from(WORKSPACE_DESTINATION),
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
)?);
|
|
|
|
|
for volume in configuration.volumes() {
|
|
|
|
|
let record = volume.record();
|
|
|
|
|
mounts.push(bind_mount(
|
|
|
|
|
host_path(&record.canonical_host_source)?,
|
|
|
|
|
PathBuf::from(&record.container_path),
|
|
|
|
|
record.access == VolumeAccess::ReadOnly,
|
|
|
|
|
false,
|
|
|
|
|
)?);
|
|
|
|
|
}
|
|
|
|
|
mounts.extend(control_mounts(control_sources)?);
|
|
|
|
|
if let Some(terminal) = terminal {
|
|
|
|
|
mounts.push(bind_mount(
|
2026-07-15 15:16:25 +09:00
|
|
|
host_path(configuration.runtime_domain_root().canonical_label())?
|
|
|
|
|
.join(&terminal.relative_source),
|
2026-07-15 14:59:43 +09:00
|
|
|
terminal.destination(),
|
|
|
|
|
true,
|
|
|
|
|
true,
|
|
|
|
|
)?);
|
|
|
|
|
}
|
|
|
|
|
Ok(mounts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
|
|
|
|
let private_namespaces = [
|
|
|
|
|
LinuxNamespaceType::Pid,
|
|
|
|
|
LinuxNamespaceType::Ipc,
|
|
|
|
|
LinuxNamespaceType::Uts,
|
|
|
|
|
LinuxNamespaceType::Mount,
|
|
|
|
|
LinuxNamespaceType::User,
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|typ| {
|
|
|
|
|
LinuxNamespaceBuilder::default()
|
|
|
|
|
.typ(typ)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)
|
|
|
|
|
})
|
|
|
|
|
.chain(std::iter::once(
|
|
|
|
|
LinuxNamespaceBuilder::default()
|
|
|
|
|
.typ(LinuxNamespaceType::Network)
|
|
|
|
|
.path(PathBuf::from(NETWORK_NAMESPACE_PATH))
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error),
|
|
|
|
|
))
|
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
|
let identity_mapping = LinuxIdMappingBuilder::default()
|
|
|
|
|
.container_id(0_u32)
|
|
|
|
|
.host_id(0_u32)
|
|
|
|
|
.size(1_u32)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)?;
|
|
|
|
|
LinuxBuilder::default()
|
|
|
|
|
.namespaces(private_namespaces)
|
|
|
|
|
.uid_mappings(vec![identity_mapping])
|
|
|
|
|
.gid_mappings(vec![identity_mapping])
|
|
|
|
|
.rootfs_propagation("rprivate")
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_runsc_command(paths: &RuntimePaths) -> RunscCommand {
|
|
|
|
|
RunscCommand {
|
|
|
|
|
program: OsString::from("runsc"),
|
|
|
|
|
arguments: vec![
|
|
|
|
|
OsString::from(format!(
|
|
|
|
|
"-root={}",
|
|
|
|
|
paths
|
|
|
|
|
.runsc_root
|
|
|
|
|
.to_str()
|
|
|
|
|
.expect("validated runtime path remains UTF-8")
|
|
|
|
|
)),
|
|
|
|
|
OsString::from("-ignore-cgroups"),
|
|
|
|
|
OsString::from("run"),
|
|
|
|
|
OsString::from(format!(
|
|
|
|
|
"-bundle={}",
|
|
|
|
|
paths
|
|
|
|
|
.bundle
|
|
|
|
|
.to_str()
|
|
|
|
|
.expect("validated runtime path remains UTF-8")
|
|
|
|
|
)),
|
|
|
|
|
OsString::from(&paths.container_name),
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_process(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
root_process: RootProcess,
|
2026-07-15 15:16:25 +09:00
|
|
|
terminal: Option<&StagedHostTerminal>,
|
2026-07-15 14:59:43 +09:00
|
|
|
) -> Result<oci_spec::runtime::Process, SandboxBuildError> {
|
|
|
|
|
let mut environment = BTreeMap::from([("PATH".to_owned(), DEFAULT_PATH.to_owned())]);
|
|
|
|
|
for (name, value) in configuration.ephemeral_environment() {
|
|
|
|
|
let value = value.expose().to_str().ok_or_else(|| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidProcess,
|
|
|
|
|
"resolved environment value is not UTF-8",
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
environment.insert(name.clone(), value.to_owned());
|
|
|
|
|
}
|
|
|
|
|
if let Some(terminal) = terminal {
|
|
|
|
|
environment.insert("TERM".to_owned(), terminal.term.clone());
|
|
|
|
|
if let Some(color_term) = &terminal.color_term {
|
|
|
|
|
environment.insert("COLORTERM".to_owned(), color_term.clone());
|
|
|
|
|
} else {
|
|
|
|
|
environment.remove("COLORTERM");
|
|
|
|
|
}
|
|
|
|
|
environment.insert(
|
|
|
|
|
"TERMINFO_DIRS".to_owned(),
|
|
|
|
|
format!("{TERMINFO_ROOT_DESTINATION}:"),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let environment = environment
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(name, value)| format!("{name}={value}"))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let empty = Capabilities::new();
|
|
|
|
|
let capabilities = LinuxCapabilitiesBuilder::default()
|
|
|
|
|
.bounding(empty.clone())
|
|
|
|
|
.effective(empty.clone())
|
|
|
|
|
.inheritable(empty.clone())
|
|
|
|
|
.permitted(empty.clone())
|
|
|
|
|
.ambient(empty)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)?;
|
|
|
|
|
let user = UserBuilder::default()
|
|
|
|
|
.uid(0_u32)
|
|
|
|
|
.gid(0_u32)
|
|
|
|
|
.umask(0o022_u32)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)?;
|
|
|
|
|
ProcessBuilder::default()
|
|
|
|
|
.terminal(false)
|
|
|
|
|
.user(user)
|
|
|
|
|
.args(root_process.arguments)
|
|
|
|
|
.env(environment)
|
|
|
|
|
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
|
|
|
|
.capabilities(capabilities)
|
|
|
|
|
.no_new_privileges(true)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn standard_mounts() -> Result<Vec<Mount>, SandboxBuildError> {
|
|
|
|
|
[
|
|
|
|
|
("/proc", "proc", "proc", vec!["nosuid", "noexec", "nodev"]),
|
|
|
|
|
(
|
|
|
|
|
"/dev",
|
|
|
|
|
"tmpfs",
|
|
|
|
|
"tmpfs",
|
|
|
|
|
vec!["nosuid", "strictatime", "mode=755", "size=65536k"],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"/dev/pts",
|
|
|
|
|
"devpts",
|
|
|
|
|
"devpts",
|
|
|
|
|
vec![
|
|
|
|
|
"nosuid",
|
|
|
|
|
"noexec",
|
|
|
|
|
"newinstance",
|
|
|
|
|
"ptmxmode=0666",
|
|
|
|
|
"mode=0620",
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"/dev/shm",
|
|
|
|
|
"tmpfs",
|
|
|
|
|
"shm",
|
|
|
|
|
vec!["nosuid", "noexec", "nodev", "mode=1777", "size=65536k"],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"/dev/mqueue",
|
|
|
|
|
"mqueue",
|
|
|
|
|
"mqueue",
|
|
|
|
|
vec!["nosuid", "noexec", "nodev"],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"/sys",
|
|
|
|
|
"sysfs",
|
|
|
|
|
"sysfs",
|
|
|
|
|
vec!["nosuid", "noexec", "nodev", "ro"],
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(destination, typ, source, options)| {
|
|
|
|
|
MountBuilder::default()
|
|
|
|
|
.destination(PathBuf::from(destination))
|
|
|
|
|
.typ(typ)
|
|
|
|
|
.source(PathBuf::from(source))
|
|
|
|
|
.options(options.into_iter().map(str::to_owned).collect::<Vec<_>>())
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn control_mounts(control_sources: &Path) -> Result<Vec<Mount>, SandboxBuildError> {
|
|
|
|
|
[
|
|
|
|
|
("credentials", CREDENTIALS_DESTINATION, true, false),
|
|
|
|
|
(
|
|
|
|
|
"mailbox/requests",
|
|
|
|
|
MAILBOX_REQUESTS_DESTINATION,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
),
|
|
|
|
|
("mailbox/results", MAILBOX_RESULTS_DESTINATION, true, false),
|
|
|
|
|
(
|
|
|
|
|
"bin/lazaret-escalate",
|
|
|
|
|
ESCALATION_CLIENT_DESTINATION,
|
|
|
|
|
true,
|
|
|
|
|
false,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(source, destination, read_only, no_exec)| {
|
|
|
|
|
bind_mount(
|
|
|
|
|
control_sources.join(source),
|
|
|
|
|
PathBuf::from(destination),
|
|
|
|
|
read_only,
|
|
|
|
|
no_exec,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn bind_mount(
|
|
|
|
|
source: PathBuf,
|
|
|
|
|
destination: PathBuf,
|
|
|
|
|
read_only: bool,
|
|
|
|
|
no_exec: bool,
|
|
|
|
|
) -> Result<Mount, SandboxBuildError> {
|
|
|
|
|
validate_absolute_normalized(&source)?;
|
|
|
|
|
validate_absolute_normalized(&destination)?;
|
|
|
|
|
let mut options = vec!["bind".to_owned(), "nosuid".to_owned(), "nodev".to_owned()];
|
|
|
|
|
options.push(if read_only { "ro" } else { "rw" }.to_owned());
|
|
|
|
|
if no_exec {
|
|
|
|
|
options.push("noexec".to_owned());
|
|
|
|
|
}
|
|
|
|
|
MountBuilder::default()
|
|
|
|
|
.destination(destination)
|
|
|
|
|
.typ("bind")
|
|
|
|
|
.source(source)
|
|
|
|
|
.options(options)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(oci_error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_network_namespace(expected: NamespaceIdentity) -> Result<(), SandboxBuildError> {
|
|
|
|
|
let observed = lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
|
|
|
|
|
.map_err(|error| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
|
|
|
|
format!("cannot inspect Runtime Supervisor network namespace: {error}"),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
if observed != expected {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
|
|
|
|
"Runtime Supervisor network namespace identity differs from readiness evidence",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildError {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_sandbox_executable(program: &str) -> Result<(), SandboxBuildError> {
|
|
|
|
|
validate_absolute_normalized(Path::new(program)).map_err(|_| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidProcess,
|
|
|
|
|
"program is not a normalized absolute sandbox path",
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_terminal_name(term: &str) -> Result<(), SandboxBuildError> {
|
|
|
|
|
let valid = !term.is_empty()
|
|
|
|
|
&& term.len() <= 64
|
|
|
|
|
&& term
|
|
|
|
|
.bytes()
|
|
|
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.' | b'_'));
|
|
|
|
|
if valid {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidTerminal,
|
|
|
|
|
"TERM violates the conservative terminal-name grammar",
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 15:16:25 +09:00
|
|
|
fn validate_compiled_terminfo(term: &str, bytes: &[u8]) -> Result<(), SandboxBuildError> {
|
|
|
|
|
if bytes.len() < 12 || bytes.len() > MAX_TERMINFO_BYTES {
|
|
|
|
|
return Err(invalid_terminfo("compiled entry has an invalid size"));
|
|
|
|
|
}
|
|
|
|
|
let magic = little_u16(bytes, 0);
|
|
|
|
|
let number_width = match magic {
|
|
|
|
|
0x011a => 2,
|
|
|
|
|
0x021e => 4,
|
|
|
|
|
_ => return Err(invalid_terminfo("compiled entry has an unknown magic")),
|
|
|
|
|
};
|
|
|
|
|
let names_size = usize::from(little_u16(bytes, 2));
|
|
|
|
|
let bool_count = usize::from(little_u16(bytes, 4));
|
|
|
|
|
let number_count = usize::from(little_u16(bytes, 6));
|
|
|
|
|
let string_count = usize::from(little_u16(bytes, 8));
|
|
|
|
|
let string_table_size = usize::from(little_u16(bytes, 10));
|
|
|
|
|
let names_end = 12_usize
|
|
|
|
|
.checked_add(names_size)
|
|
|
|
|
.filter(|end| *end <= bytes.len())
|
|
|
|
|
.ok_or_else(|| invalid_terminfo("compiled entry names exceed its length"))?;
|
|
|
|
|
let names = &bytes[12..names_end];
|
|
|
|
|
if names.last() != Some(&0)
|
|
|
|
|
|| !names[..names.len().saturating_sub(1)]
|
|
|
|
|
.split(|byte| *byte == b'|')
|
|
|
|
|
.any(|name| name == term.as_bytes())
|
|
|
|
|
{
|
|
|
|
|
return Err(invalid_terminfo(
|
|
|
|
|
"compiled entry does not name the requested terminal",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let after_bools = names_end
|
|
|
|
|
.checked_add(bool_count)
|
|
|
|
|
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
|
|
|
|
let aligned = after_bools
|
|
|
|
|
.checked_add(after_bools % 2)
|
|
|
|
|
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
|
|
|
|
let required = aligned
|
|
|
|
|
.checked_add(
|
|
|
|
|
number_count
|
|
|
|
|
.checked_mul(number_width)
|
|
|
|
|
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?,
|
|
|
|
|
)
|
|
|
|
|
.and_then(|size| size.checked_add(string_count.checked_mul(2)?))
|
|
|
|
|
.and_then(|size| size.checked_add(string_table_size))
|
|
|
|
|
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
|
|
|
|
if required > bytes.len() {
|
|
|
|
|
return Err(invalid_terminfo("compiled entry sections are truncated"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn little_u16(bytes: &[u8], offset: usize) -> u16 {
|
|
|
|
|
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn invalid_terminfo(detail: &str) -> SandboxBuildError {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::InvalidTerminal, detail)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_or_create_staging_directory(
|
|
|
|
|
parent: OwnedFd,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Result<OwnedFd, SandboxBuildError> {
|
|
|
|
|
match rustix::fs::mkdirat(&parent, name, Mode::RWXU) {
|
|
|
|
|
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
|
|
|
|
|
Err(error) => return Err(staging_error(error)),
|
|
|
|
|
}
|
|
|
|
|
let directory = openat2(
|
|
|
|
|
parent,
|
|
|
|
|
name,
|
|
|
|
|
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
|
|
|
|
Mode::empty(),
|
|
|
|
|
confined_resolve_flags(),
|
|
|
|
|
)
|
|
|
|
|
.map_err(staging_error)?;
|
|
|
|
|
let stat = rustix::fs::fstat(&directory).map_err(staging_error)?;
|
|
|
|
|
if !FileType::from_raw_mode(stat.st_mode).is_dir()
|
|
|
|
|
|| stat.st_uid != rustix::process::getuid().as_raw()
|
|
|
|
|
|| stat.st_mode & 0o7777 != 0o700
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"terminfo staging directory has unsafe ownership or mode",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(directory)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stage_exact_file(
|
|
|
|
|
directory: &OwnedFd,
|
|
|
|
|
name: &str,
|
|
|
|
|
bytes: &[u8],
|
|
|
|
|
) -> Result<OwnedFd, SandboxBuildError> {
|
|
|
|
|
match open_staged_file(directory, name) {
|
|
|
|
|
Ok(descriptor) => {
|
|
|
|
|
require_staged_file(&descriptor)?;
|
|
|
|
|
if read_descriptor(descriptor)? != bytes {
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"existing staged terminfo artifact has different bytes",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
open_staged_file(directory, name).map_err(staging_error)
|
|
|
|
|
}
|
|
|
|
|
Err(rustix::io::Errno::NOENT) => {
|
|
|
|
|
let descriptor = rustix::fs::openat(
|
|
|
|
|
directory,
|
|
|
|
|
name,
|
|
|
|
|
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
|
|
|
|
Mode::RUSR | Mode::WUSR,
|
|
|
|
|
)
|
|
|
|
|
.map_err(staging_error)?;
|
|
|
|
|
let mut file = File::from(descriptor);
|
|
|
|
|
let result = (|| {
|
|
|
|
|
file.write_all(bytes).map_err(staging_error)?;
|
|
|
|
|
rustix::fs::fchmod(file.as_fd(), Mode::RUSR).map_err(staging_error)?;
|
|
|
|
|
file.sync_all().map_err(staging_error)
|
|
|
|
|
})();
|
|
|
|
|
if let Err(error) = result {
|
|
|
|
|
drop(file);
|
|
|
|
|
let _ = rustix::fs::unlinkat(directory, name, AtFlags::empty());
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
drop(file);
|
|
|
|
|
rustix::fs::fsync(directory).map_err(staging_error)?;
|
|
|
|
|
let descriptor = open_staged_file(directory, name).map_err(staging_error)?;
|
|
|
|
|
require_staged_file(&descriptor)?;
|
|
|
|
|
Ok(descriptor)
|
|
|
|
|
}
|
|
|
|
|
Err(error) => Err(staging_error(error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_staged_file(directory: &OwnedFd, name: &str) -> rustix::io::Result<OwnedFd> {
|
|
|
|
|
openat2(
|
|
|
|
|
directory,
|
|
|
|
|
name,
|
|
|
|
|
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
|
|
|
|
Mode::empty(),
|
|
|
|
|
confined_resolve_flags(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_staged_file(descriptor: &OwnedFd) -> Result<(), SandboxBuildError> {
|
|
|
|
|
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
|
|
|
|
if !FileType::from_raw_mode(stat.st_mode).is_file()
|
|
|
|
|
|| stat.st_uid != rustix::process::getuid().as_raw()
|
|
|
|
|
|| stat.st_mode & 0o7777 != 0o400
|
|
|
|
|
|| stat.st_nlink != 1
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::ChangedAuthority,
|
|
|
|
|
"staged terminfo artifact has unsafe type, ownership, mode, or link count",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_descriptor(descriptor: OwnedFd) -> Result<Vec<u8>, SandboxBuildError> {
|
|
|
|
|
let mut bytes = Vec::new();
|
|
|
|
|
File::from(descriptor)
|
|
|
|
|
.take((MAX_TERMINFO_BYTES + 1) as u64)
|
|
|
|
|
.read_to_end(&mut bytes)
|
|
|
|
|
.map_err(staging_error)?;
|
|
|
|
|
if bytes.len() > MAX_TERMINFO_BYTES {
|
|
|
|
|
return Err(invalid_terminfo("staged terminfo artifact is oversized"));
|
|
|
|
|
}
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn confined_resolve_flags() -> ResolveFlags {
|
|
|
|
|
ResolveFlags::BENEATH
|
|
|
|
|
| ResolveFlags::NO_MAGICLINKS
|
|
|
|
|
| ResolveFlags::NO_SYMLINKS
|
|
|
|
|
| ResolveFlags::NO_XDEV
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn staging_error(error: impl fmt::Display) -> SandboxBuildError {
|
|
|
|
|
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 14:59:43 +09:00
|
|
|
fn validate_absolute_normalized(path: &Path) -> Result<(), SandboxBuildError> {
|
|
|
|
|
if !path.is_absolute()
|
|
|
|
|
|| path.as_os_str().as_bytes().contains(&0)
|
|
|
|
|
|| path
|
|
|
|
|
.components()
|
|
|
|
|
.any(|component| !matches!(component, Component::RootDir | Component::Normal(_)))
|
|
|
|
|
{
|
|
|
|
|
return Err(SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidPath,
|
|
|
|
|
"path is not normalized and absolute",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
utf8_path(path)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn utf8_path(path: &Path) -> Result<&str, SandboxBuildError> {
|
|
|
|
|
path.to_str().ok_or_else(|| {
|
|
|
|
|
SandboxBuildError::new(
|
|
|
|
|
SandboxBuildErrorKind::InvalidPath,
|
|
|
|
|
"OCI JSON path is not UTF-8",
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn host_path(path: &lazaret_core::identity::HostPath) -> Result<PathBuf, SandboxBuildError> {
|
|
|
|
|
let path = PathBuf::from(OsString::from_vec(path.as_bytes().to_vec()));
|
|
|
|
|
validate_absolute_normalized(&path)?;
|
|
|
|
|
Ok(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn hex_lower(bytes: &[u8]) -> String {
|
|
|
|
|
let mut output = String::with_capacity(bytes.len() * 2);
|
|
|
|
|
for byte in bytes {
|
|
|
|
|
use fmt::Write as _;
|
|
|
|
|
write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
|
|
|
|
|
}
|
|
|
|
|
output
|
|
|
|
|
}
|