Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 66 additions & 11 deletions rust/crates/runtime/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,18 @@ pub fn build_linux_sandbox_command(
return None;
}

let mut args = vec![
"--user".to_string(),
"--map-root-user".to_string(),
let mut args: Vec<String> = working_unshare_mapping()
.unwrap_or(UNSHARE_MAPPING_CANDIDATES[0])
.iter()
.map(|arg| arg.to_string())
.collect();
args.extend([
"--mount".to_string(),
"--ipc".to_string(),
"--pid".to_string(),
"--uts".to_string(),
"--fork".to_string(),
];
]);
if status.network_active {
args.push("--net".to_string());
}
Expand Down Expand Up @@ -282,6 +285,49 @@ fn command_exists(command: &str) -> bool {
.is_some_and(|paths| env::split_paths(&paths).any(|path| path.join(command).exists()))
}

/// Candidate `unshare` user-namespace mapping options, in preference order.
///
/// Most systems accept `--map-root-user` alone. On kernels or containers that
/// block unprivileged writes to `/proc/self/uid_map` (e.g. GitHub Actions,
/// restricted AppArmor profiles), util-linux instead delegates to the setuid
/// `newuidmap`/`newgidmap` helpers when `--map-auto` is also present.
///
/// That fallback therefore depends on the setuid helpers (the `uidmap`
/// package on Debian/Ubuntu) and on the current user having a range in
/// `/etc/subuid` and `/etc/subgid`. When either is missing, `--map-auto`
/// fails and the startup probe rejects the candidate, keeping the plain form.
const UNSHARE_MAPPING_CANDIDATES: &[&[&str]] = &[
&["--user", "--map-root-user"],
&["--user", "--map-root-user", "--map-auto"],
];

/// Probe a candidate `unshare` mapping invocation with a trivial program.
fn unshare_probe(args: &[&str]) -> bool {
std::process::Command::new("unshare")
.args(args)
.arg("true")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}

/// The first mapping option set that works on this machine, if any.
///
/// Probes are cached for the process lifetime; a missing `unshare` binary or a
/// kernel that refuses every mapping yields `None`.
fn working_unshare_mapping() -> Option<&'static [&'static str]> {
use std::sync::OnceLock;
static MAPPING: OnceLock<Option<&'static [&'static str]>> = OnceLock::new();
*MAPPING.get_or_init(|| {
UNSHARE_MAPPING_CANDIDATES
.iter()
.copied()
.find(|args| unshare_probe(args))
})
}

/// Check whether `unshare --user` actually works on this system.
/// On some CI environments (e.g. GitHub Actions), the binary exists but
/// user namespaces are restricted, causing silent failures.
Expand All @@ -292,13 +338,7 @@ fn unshare_user_namespace_works() -> bool {
if !command_exists("unshare") {
return false;
}
std::process::Command::new("unshare")
.args(["--user", "--map-root-user", "true"])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
working_unshare_mapping().is_some()
})
}

Expand Down Expand Up @@ -359,6 +399,21 @@ mod tests {
assert_eq!(request.allowed_mounts, vec!["tmp"]);
}

#[test]
fn mapping_candidates_prefer_plain_root_mapping() {
assert!(!super::UNSHARE_MAPPING_CANDIDATES.is_empty());
for candidate in super::UNSHARE_MAPPING_CANDIDATES {
assert!(candidate.contains(&"--user"));
assert!(candidate.contains(&"--map-root-user"));
}
// The plain form must be tried first; `--map-auto` is only a fallback
// for kernels/containers that block unprivileged uid_map writes.
assert_eq!(
super::UNSHARE_MAPPING_CANDIDATES[0],
&["--user", "--map-root-user"]
);
}

#[test]
fn builds_linux_launcher_with_network_flag_when_requested() {
let config = SandboxConfig::default();
Expand Down