diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 7713febb6f..9545ec1fd4 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -172,6 +172,35 @@ jobs: - name: Test run: mise run test:python + rootless-caps: + name: Rootless capability tests + needs: pr_metadata + if: needs.pr_metadata.outputs.should_run == 'true' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 + with: + toolchain: "1.95.0" + + - name: Run supervisor capability tests without CAP_SETPCAP + run: | + sudo useradd -m testuser + sudo chmod a+rx /home/runner /home/runner/work /home/runner/work/OpenShell + sudo chmod -R a+rX "$GITHUB_WORKSPACE" + sudo cp -r /home/runner/.rustup /home/testuser/.rustup + sudo chown -R testuser: /home/testuser/.rustup + sudo mkdir -p /home/testuser/.cargo + sudo cp /home/runner/.cargo/config.toml /home/testuser/.cargo/ 2>/dev/null || true + sudo chown -R testuser: /home/testuser/.cargo + sudo -u testuser env \ + PATH="/home/testuser/.cargo/bin:/home/testuser/.rustup/toolchains/1.95.0-x86_64-unknown-linux-gnu/bin:$PATH" \ + CARGO_HOME="/home/testuser/.cargo" \ + RUSTUP_HOME="/home/testuser/.rustup" \ + CARGO_TARGET_DIR="/home/testuser/target" \ + bash -c "cd $GITHUB_WORKSPACE && cargo test -p openshell-supervisor-process --lib -- capability_bounding drop_privileges" + markdown: name: Markdown needs: pr_metadata diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index c1b6b45328..d228797e6e 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -26,7 +26,7 @@ use std::process::Stdio; #[cfg(target_os = "linux")] use std::sync::OnceLock; use tokio::process::{Child, Command}; -use tracing::debug; +use tracing::{debug, warn}; const SUPERVISOR_ONLY_ENV_VARS: &[&str] = &[ openshell_core::sandbox_env::SANDBOX_TOKEN, @@ -189,6 +189,14 @@ fn validate_capability_bounding_set_clear( "Failed to clear unknown child capability bounding set entries: {unknown_err}" )), }, + Err(err) if err.code() == libc::EPERM => { + warn!( + ?remaining, + "CAP_SETPCAP is unavailable and the child capability bounding set is non-empty; \ + the child process relies on seccomp and Landlock for confinement" + ); + Ok(()) + } Err(err) => Err(miette::miette!( "Failed to clear child capability bounding set: {err}" )), @@ -1150,22 +1158,17 @@ mod tests { #[test] #[cfg(target_os = "linux")] - fn capability_bounding_set_clear_rejects_nonempty_eperm() { + fn capability_bounding_set_clear_tolerates_nonempty_eperm() { let mut remaining = capctl::caps::CapSet::empty(); remaining.add(capctl::caps::Cap::CHOWN); - let result = validate_capability_bounding_set_clear( - Err(capctl::Error::from_code(libc::EPERM)), - remaining, - || panic!("unknown capabilities should not be checked when known caps remain"), - ); - - assert!(result.is_err()); assert!( - result - .unwrap_err() - .to_string() - .contains("Failed to clear child capability bounding set") + validate_capability_bounding_set_clear( + Err(capctl::Error::from_code(libc::EPERM)), + remaining, + || panic!("unknown capabilities should not be checked when known caps remain"), + ) + .is_ok() ); } @@ -1270,21 +1273,7 @@ mod tests { let result = drop_privileges(&policy); - #[cfg(target_os = "linux")] - { - if capability_bounding_set_clear_available() { - assert!(result.is_ok(), "drop_privileges failed: {result:?}"); - } else { - let msg = format!("{}", result.unwrap_err()); - assert!( - msg.contains("Failed to clear child capability bounding set"), - "unexpected failure: {msg}" - ); - } - } - - #[cfg(not(target_os = "linux"))] - assert!(result.is_ok()); + assert!(result.is_ok(), "drop_privileges failed: {result:?}"); } #[test]