use std::io;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use crate::orchestration::CapabilityPolicy;
use crate::process_sandbox::{NETNS_LAUNCH_SUBCOMMAND, NETNS_RULESET_FD_FLAG, NETNS_SECCOMP_FLAG};
use crate::VmError;
use super::{sandbox_rejection, LandlockProfile, ProcessProfile, TransferableConfinement};
use crate::stdlib::sandbox::backend::PrepareOutcome;
pub enum ReexecConfinement {
BeforeExec(TransferableConfinement),
AfterNamespace(TransferableConfinement),
}
pub(super) fn namespaced_outcome(
launcher: PathBuf,
program: &str,
args: &[String],
prep: &mut ProcessProfile,
) -> PrepareOutcome {
let confinement = TransferableConfinement {
ruleset: prep.landlock.take().map(LandlockProfile::take_ruleset),
seccomp: std::mem::take(&mut prep.seccomp),
};
let argv = namespaced_launcher_argv(program, args, &confinement);
PrepareOutcome::NamespacedExec {
wrapper: launcher.display().to_string(),
args: argv,
confinement,
}
}
pub(in crate::stdlib::sandbox) fn resolve_netns_launcher(
policy: &CapabilityPolicy,
) -> Result<Option<PathBuf>, VmError> {
if !policy.process_sandbox.allow_tcp_loopback {
return Ok(None);
}
let Some(path) = policy.process_sandbox.netns_launcher_path.as_ref() else {
return Err(sandbox_rejection(
"TCP loopback-only child networking needs a private network namespace, which only \
the namespace helper can build; no helper path was supplied, so the grant is \
refused rather than widened"
.to_string(),
));
};
let path = PathBuf::from(path);
if !path.is_absolute() {
return Err(sandbox_rejection(format!(
"the namespace helper path must be absolute so the host policy grant names one \
file; got {}",
path.display()
)));
}
if !path.is_file() {
return Err(sandbox_rejection(format!(
"TCP loopback-only child networking needs the namespace helper at {}, which is not \
an existing file on this host; the grant is refused rather than widened",
path.display()
)));
}
Ok(Some(path))
}
pub(super) fn namespaced_launcher_argv(
payload_program: &str,
payload_args: &[String],
confinement: &TransferableConfinement,
) -> Vec<String> {
launcher_argv_with_ruleset(
payload_program,
payload_args,
confinement,
confinement.ruleset_fd(),
)
}
pub(in crate::stdlib::sandbox) fn launcher_argv_with_ruleset(
payload_program: &str,
payload_args: &[String],
confinement: &TransferableConfinement,
ruleset_fd: Option<i32>,
) -> Vec<String> {
let mut argv = vec![NETNS_LAUNCH_SUBCOMMAND.to_string()];
if let Some(fd) = ruleset_fd {
argv.push(NETNS_RULESET_FD_FLAG.to_string());
argv.push(fd.to_string());
}
argv.push(NETNS_SECCOMP_FLAG.to_string());
argv.push(hex_encode(&confinement.seccomp_bytes()));
argv.push("--".to_string());
argv.push(payload_program.to_string());
argv.extend(payload_args.iter().cloned());
argv
}
fn hex_encode(bytes: &[u8]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(DIGITS[usize::from(byte >> 4)] as char);
out.push(DIGITS[usize::from(byte & 0x0f)] as char);
}
out
}
pub fn decode_seccomp_hex(text: &str) -> io::Result<Vec<u8>> {
if !text.len().is_multiple_of(2) {
return Err(io::Error::other(
"transferred seccomp program is not a whole number of bytes",
));
}
let mut bytes = Vec::with_capacity(text.len() / 2);
let raw = text.as_bytes();
for pair in raw.chunks_exact(2) {
let hi = (pair[0] as char).to_digit(16);
let lo = (pair[1] as char).to_digit(16);
match (hi, lo) {
(Some(hi), Some(lo)) => bytes.push(((hi << 4) | lo) as u8),
_ => {
return Err(io::Error::other(
"transferred seccomp program is not hexadecimal",
))
}
}
}
Ok(bytes)
}
pub fn keep_ruleset_across_exec(command: &mut Command, confinement: TransferableConfinement) {
let Some(hook) = clear_cloexec_hook(confinement) else {
return;
};
unsafe {
command.pre_exec(hook);
}
}
pub(crate) fn keep_ruleset_across_exec_tokio(
command: &mut tokio::process::Command,
confinement: TransferableConfinement,
) {
let Some(hook) = clear_cloexec_hook(confinement) else {
return;
};
unsafe {
command.pre_exec(hook);
}
}
fn clear_cloexec_hook(
confinement: TransferableConfinement,
) -> Option<impl FnMut() -> io::Result<()> + Send + Sync + 'static> {
let fd = confinement.ruleset_fd()?;
Some(move || {
let _keep_open = &confinement;
if unsafe { libc::fcntl(fd, libc::F_SETFD, 0) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
})
}
pub(super) fn namespaced_loopback_grant(policy: &CapabilityPolicy) -> bool {
policy.process_sandbox.allow_tcp_loopback
}