diff --git a/.github/workflows/probe-unshare.yml b/.github/workflows/probe-unshare.yml new file mode 100644 index 0000000000..ec9d5f490b --- /dev/null +++ b/.github/workflows/probe-unshare.yml @@ -0,0 +1,28 @@ +name: probe-unshare +on: + workflow_dispatch: +jobs: + probe: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Probe unshare semantics + run: | + echo "=== sysctls ===" + cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null || echo "no unprivileged_userns_clone" + cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null || echo "no apparmor_restrict_unprivileged_userns" + echo "=== identity ===" + id + echo "=== subuids ===" + getent passwd "$(id -un)" | head -1 + grep -E "$(id -un)|^$(id -u):" /etc/subuid /etc/subgid 2>/dev/null || echo "no subuid/subgid entries" + echo "=== helpers ===" + which unshare newuidmap newgidmap + echo "=== plain ===" + unshare --user --map-root-user true; echo "plain rc=$?" + echo "=== map-auto ===" + unshare --user --map-root-user --map-auto true; echo "auto rc=$?" + echo "=== full launch shape (map-auto) ===" + unshare --user --map-root-user --map-auto --mount --ipc --pid --uts --fork sh -lc "echo alpha"; echo "full rc=$?" + echo "=== full launch shape (plain) ===" + unshare --user --map-root-user --mount --ipc --pid --uts --fork sh -lc "echo alpha"; echo "plain full rc=$?" diff --git a/rust/crates/runtime/Cargo.toml b/rust/crates/runtime/Cargo.toml index 38436133e7..8ec48b61f8 100644 --- a/rust/crates/runtime/Cargo.toml +++ b/rust/crates/runtime/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true publish.workspace = true [dependencies] +libc = "0.2" sha2 = "0.10" glob = "0.3" plugins = { path = "../plugins" } diff --git a/rust/crates/runtime/tests/probe_unshare.rs b/rust/crates/runtime/tests/probe_unshare.rs new file mode 100644 index 0000000000..072aceb03e --- /dev/null +++ b/rust/crates/runtime/tests/probe_unshare.rs @@ -0,0 +1,61 @@ +//! Scratch probe: dump GitHub runner unshare semantics (temporary, PRs will be closed). +#![cfg(target_os = "linux")] + +use std::process::Command; + +fn run(args: &[&str]) -> (i32, String, String) { + let out = Command::new("unshare").args(args).output(); + match out { + Ok(o) => ( + o.status.code().unwrap_or(-1), + String::from_utf8_lossy(&o.stdout).trim().to_string(), + String::from_utf8_lossy(&o.stderr).trim().to_string(), + ), + Err(e) => (-1, String::new(), format!("spawn error: {e}")), + } +} + +fn sh(cmd: &str) -> String { + Command::new("sh") + .args(["-lc", cmd]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_default() +} + +#[test] +fn dump_unshare_semantics() { + let mut report = String::new(); + report.push_str(&format!("uid line: {}\n", sh("id"))); + for f in ["/etc/subuid", "/etc/subgid"] { + report.push_str(&format!("--- {f} ---\n")); + if let Ok(s) = std::fs::read_to_string(f) { + report.push_str(&s); + } else { + report.push_str("(unreadable)\n"); + } + } + for k in ["/proc/sys/kernel/unprivileged_userns_clone", "/proc/sys/kernel/apparmor_restrict_unprivileged_userns"] { + report.push_str(&format!("{k} = {}\n", std::fs::read_to_string(k).unwrap_or_else(|_| "(n/a)".into()))); + } + for (name, args) in [ + ("plain", &["--user", "--map-root-user", "true"][..]), + ("auto", &["--user", "--map-root-user", "--map-auto", "true"][..]), + ( + "plain-full", + &["--user", "--map-root-user", "--mount", "--ipc", "--pid", "--uts", "--fork", "sh", "-lc", "echo alpha"][..], + ), + ( + "auto-full", + &["--user", "--map-root-user", "--map-auto", "--mount", "--ipc", "--pid", "--uts", "--fork", "sh", "-lc", "echo alpha"][..], + ), + ( + "auto-full-echo-multi", + &["--user", "--map-root-user", "--map-auto", "--mount", "--ipc", "--pid", "--uts", "--fork", "sh", "-lc", "echo alpha from bash"][..], + ), + ] { + let (rc, so, se) = run(args); + report.push_str(&format!("[{name}] rc={rc} stdout={so:?} stderr={se:?}\n")); + } + panic!("PROBE REPORT:\n{report}"); +}