Decompose oversized Rust files by responsibility
This commit is contained in:
parent
03bfe92ca0
commit
ee9201b057
1 changed files with 18648 additions and 18513 deletions
|
|
@ -709,777 +709,6 @@ fn build_sandbox_bundle_from_source(
|
|||
})
|
||||
}
|
||||
|
||||
struct RuntimePaths {
|
||||
container_name: String,
|
||||
bundle: PathBuf,
|
||||
runsc_root: PathBuf,
|
||||
overlay: PathBuf,
|
||||
control_sources: PathBuf,
|
||||
}
|
||||
|
||||
fn handoff_runtime_sources(
|
||||
paths: &RuntimePaths,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
specification_json: &[u8],
|
||||
) -> Vec<RuntimeHandoffSource> {
|
||||
let container = Path::new("sandbox-control").join(&paths.container_name);
|
||||
let mut sources = [
|
||||
(
|
||||
Path::new("bundles").join(&paths.container_name),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(
|
||||
Path::new("bundles")
|
||||
.join(&paths.container_name)
|
||||
.join("config.json"),
|
||||
HandoffPathType::RegularFile,
|
||||
),
|
||||
(PathBuf::from("runsc"), HandoffPathType::Directory),
|
||||
(
|
||||
PathBuf::from("runsc-overlay").join(&paths.container_name),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(container.clone(), HandoffPathType::Directory),
|
||||
(container.join("credentials"), HandoffPathType::Directory),
|
||||
(
|
||||
container.join("mailbox/requests"),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(
|
||||
container.join("mailbox/results"),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(
|
||||
container.join("bin/lazaret-escalate"),
|
||||
HandoffPathType::RegularFile,
|
||||
),
|
||||
(
|
||||
container.join("bin/lazaret-sandbox-init"),
|
||||
HandoffPathType::RegularFile,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(relative_path, expected_type)| {
|
||||
let expected_bytes = (relative_path
|
||||
== Path::new("bundles")
|
||||
.join(&paths.container_name)
|
||||
.join("config.json"))
|
||||
.then(|| specification_json.to_vec());
|
||||
RuntimeHandoffSource {
|
||||
relative_path,
|
||||
expected_type,
|
||||
build_identity: None,
|
||||
expected_bytes,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
sources.push(RuntimeHandoffSource {
|
||||
relative_path: Path::new("state-anchor/runtime-files").join(name),
|
||||
expected_type: HandoffPathType::RegularFile,
|
||||
build_identity: None,
|
||||
expected_bytes: None,
|
||||
});
|
||||
}
|
||||
if let Some(terminal) = terminal {
|
||||
sources.push(RuntimeHandoffSource {
|
||||
relative_path: terminal.relative_source.clone(),
|
||||
expected_type: HandoffPathType::RegularFile,
|
||||
build_identity: Some(terminal.identity),
|
||||
expected_bytes: None,
|
||||
});
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
fn open_runtime_source(
|
||||
runtime_root: &OwnedFd,
|
||||
relative_path: &Path,
|
||||
expected_type: HandoffPathType,
|
||||
readable: bool,
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
let descriptor = openat2(
|
||||
runtime_root,
|
||||
relative_path,
|
||||
if readable {
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW
|
||||
} else {
|
||||
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW
|
||||
},
|
||||
Mode::empty(),
|
||||
ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)
|
||||
.map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!(
|
||||
"runtime handoff source {}: {error}",
|
||||
relative_path.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let stat = rustix::fs::fstat(&descriptor).map_err(staging_error)?;
|
||||
let actual = FileType::from_raw_mode(stat.st_mode);
|
||||
let matches = match expected_type {
|
||||
HandoffPathType::Directory => actual.is_dir(),
|
||||
HandoffPathType::RegularFile => actual.is_file(),
|
||||
};
|
||||
if !matches {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"runtime handoff source changed type",
|
||||
));
|
||||
}
|
||||
Ok(descriptor)
|
||||
}
|
||||
|
||||
fn require_exact_bytes(descriptor: OwnedFd, expected: &[u8]) -> Result<(), SandboxBuildError> {
|
||||
let limit = u64::try_from(expected.len())
|
||||
.ok()
|
||||
.and_then(|length| length.checked_add(1))
|
||||
.ok_or_else(|| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged OCI configuration length is invalid",
|
||||
)
|
||||
})?;
|
||||
let mut observed = Vec::new();
|
||||
File::from(descriptor)
|
||||
.take(limit)
|
||||
.read_to_end(&mut observed)
|
||||
.map_err(staging_error)?;
|
||||
if observed != expected {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged OCI configuration differs from its bundle",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn descriptor_identity(descriptor: &OwnedFd) -> Result<FileIdentity, SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
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",
|
||||
));
|
||||
}
|
||||
Ok(FileIdentity {
|
||||
device: stat.st_dev,
|
||||
inode: stat.st_ino,
|
||||
mount_id: extended.stx_mnt_id,
|
||||
})
|
||||
}
|
||||
|
||||
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"),
|
||||
overlay: runtime_domain.join("runsc-overlay").join(&container_name),
|
||||
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
||||
container_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_mounts(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
control_sources: &Path,
|
||||
workspace_source: PathBuf,
|
||||
) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
let mut mounts = standard_mounts()?;
|
||||
mounts.push(bind_mount(
|
||||
workspace_source,
|
||||
PathBuf::from(WORKSPACE_DESTINATION),
|
||||
false,
|
||||
false,
|
||||
)?);
|
||||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||
for destination in &configuration.fingerprint_record().persistent_dirs {
|
||||
mounts.push(bind_mount(
|
||||
runtime_domain
|
||||
.join("state-anchor/merged")
|
||||
.join(destination.strip_prefix('/').unwrap_or(destination)),
|
||||
PathBuf::from(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)?);
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
mounts.push(bind_mount(
|
||||
runtime_domain.join("state-anchor/runtime-files").join(name),
|
||||
Path::new("/etc").join(name),
|
||||
true,
|
||||
true,
|
||||
)?);
|
||||
}
|
||||
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<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 uid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
let gid_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![uid_mapping])
|
||||
.gid_mappings(vec![gid_mapping])
|
||||
.rootfs_propagation("rprivate")
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) -> RunscCommand {
|
||||
RunscCommand::new(
|
||||
"runsc",
|
||||
[
|
||||
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(format!(
|
||||
"-overlay2=root:dir={}",
|
||||
paths
|
||||
.overlay
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from("run"),
|
||||
OsString::from(format!(
|
||||
"-bundle={}",
|
||||
paths
|
||||
.bundle
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from(&paths.container_name),
|
||||
],
|
||||
bundle_authorization,
|
||||
)
|
||||
}
|
||||
|
||||
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, SandboxBuildError> {
|
||||
validate_absolute_normalized(path)?;
|
||||
openat2(
|
||||
rustix::fs::CWD,
|
||||
path,
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)
|
||||
.map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("prepared root {}: {error}", path.display()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_process(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
root_process: RootProcess,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
) -> 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,
|
||||
),
|
||||
(
|
||||
"bin/lazaret-sandbox-init",
|
||||
SANDBOX_INIT_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 changed_authority_at(label: &str, error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("{label}: {error}"),
|
||||
)
|
||||
}
|
||||
|
||||
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<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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
include!("sandbox/runtime_sources.rs");
|
||||
include!("sandbox/mounts.rs");
|
||||
include!("sandbox/validation.rs");
|
||||
|
|
|
|||
Loading…
Reference in a new issue