Skip to content
Closed
Show file tree
Hide file tree
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
29 changes: 29 additions & 0 deletions .github/workflows/branch-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 17 additions & 28 deletions crates/openshell-supervisor-process/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}"
)),
Expand Down Expand Up @@ -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()
);
}

Expand Down Expand Up @@ -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]
Expand Down
Loading