use std::collections::BTreeMap; use std::ffi::{OsStr, OsString}; use std::fmt; use std::fs::File; use std::io::{Read, Write}; use std::os::fd::{AsFd, OwnedFd}; 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, }; use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, openat2}; 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"; const MAX_TERMINFO_BYTES: usize = 1_048_576; #[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) -> 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, } 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, arguments: impl IntoIterator, ) -> Result { 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 { fn from_descriptor(descriptor: &OwnedFd) -> Result { 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", )); } Ok(Self { device: stat.st_dev, inode: stat.st_ino, }) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct HostTerminal { term: String, color_term: Option, compiled_entry: Vec, } impl HostTerminal { /// Accepts one bounded compiled terminfo entry without accepting a host path. /// /// # Errors /// Invalid names, colors, terminfo headers, aliases, and oversized entries are refused. pub fn from_compiled_entry( term: impl Into, color_term: Option, compiled_entry: Vec, ) -> Result { 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", )); } validate_compiled_terminfo(&term, &compiled_entry)?; Ok(Self { term, color_term, compiled_entry, }) } } #[derive(Debug)] pub struct StagedHostTerminal { term: String, color_term: Option, compiled_entry: Vec, 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 { return Err(SandboxBuildError::new( SandboxBuildErrorKind::ChangedAuthority, "staged terminfo artifact changed identity or bytes", )); } 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) } } /// 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 { 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, }) } pub struct SandboxBuildRequest<'a> { pub configuration: &'a ResolvedDomainConfiguration, pub container_id: ContainerId, pub expected_network_namespace: NamespaceIdentity, pub root_process: RootProcess, pub terminal: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunscCommand { program: OsString, arguments: Vec, } 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, 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::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 { 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 { terminal.revalidate(request.configuration)?; } 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 { 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"), control_sources: runtime_domain.join("sandbox-control").join(&container_name), container_name, }) } fn build_mounts( configuration: &ResolvedDomainConfiguration, terminal: Option<&StagedHostTerminal>, control_sources: &Path, ) -> Result, 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( host_path(configuration.runtime_domain_root().canonical_label())? .join(&terminal.relative_source), terminal.destination(), true, true, )?); } Ok(mounts) } fn build_linux() -> Result { 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::, _>>()?; 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("-rootless"), OsString::from("-network=host"), 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, terminal: Option<&StagedHostTerminal>, ) -> Result { 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::>(); 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, 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::>()) .build() .map_err(oci_error) }) .collect() } fn control_mounts(control_sources: &Path) -> Result, 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 { 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", )) } } 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 { 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 { 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 { 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, 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()) } 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 { 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 }