Ensure root filesystem is always read-only inside sandbox

Whitelist mode's implicit bwrap root was a writable tmpfs, letting the
sandboxed process create files and directories anywhere not covered by
an explicit ro mount. This was not an issue in blacklist mode due to
--ro-bind / / covering that case.

This patch adds --remount-ro / before any other mount to make the base
layer read-only in both modes.
This commit is contained in:
2026-03-29 16:50:59 +02:00
parent 389e38a800
commit f1d7a14b8d
2 changed files with 102 additions and 0 deletions

View File

@@ -34,6 +34,7 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
add_ro_bind(&mut cmd, path)?; add_ro_bind(&mut cmd, path)?;
} }
cmd.args(["--remount-ro", "/"]);
cmd.arg("--new-session"); cmd.arg("--new-session");
cmd.arg("--die-with-parent"); cmd.arg("--die-with-parent");
cmd.arg("--chdir").arg(&config.chdir); cmd.arg("--chdir").arg(&config.chdir);

View File

@@ -420,6 +420,107 @@ fn blacklist_dev_input_hidden() {
); );
} }
#[test]
fn blacklist_root_is_readonly() {
let output = sandbox(&[])
.args([
"--",
"bash",
"-c",
"touch /rootfile 2>&1 && echo WRITABLE || echo READONLY; \
mkdir /newdir 2>&1 && echo MKDIR_OK || echo MKDIR_FAIL",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("READONLY"),
"expected root to be read-only in blacklist mode, got: {stdout}"
);
assert!(
stdout.contains("MKDIR_FAIL"),
"expected mkdir at root to fail in blacklist mode, got: {stdout}"
);
}
#[test]
fn whitelist_root_is_readonly() {
let output = sandbox(&["--whitelist"])
.args([
"--",
"bash",
"-c",
"touch /rootfile 2>&1 && echo WRITABLE || echo READONLY; \
mkdir /newdir 2>&1 && echo MKDIR_OK || echo MKDIR_FAIL",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("READONLY"),
"expected root to be read-only in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("MKDIR_FAIL"),
"expected mkdir at root to fail in whitelist mode, got: {stdout}"
);
}
#[test]
fn whitelist_mountpoint_parents_are_readonly() {
let output = sandbox(&["--whitelist"])
.args([
"--",
"bash",
"-c",
"echo pwned > /home/testfile 2>&1 && echo HOME_WRITABLE || echo HOME_READONLY; \
touch /etc/newfile 2>&1 && echo ETC_WRITABLE || echo ETC_READONLY; \
touch /var/newfile 2>&1 && echo VAR_WRITABLE || echo VAR_READONLY",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HOME_READONLY"),
"expected /home to be read-only in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("ETC_READONLY"),
"expected /etc to be read-only in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("VAR_READONLY"),
"expected /var to be read-only in whitelist mode, got: {stdout}"
);
}
#[test]
fn whitelist_tmp_still_writable() {
let output = sandbox(&["--whitelist"])
.args([
"--",
"bash",
"-c",
"touch /tmp/ok 2>&1 && echo TMP_WRITABLE || echo TMP_READONLY; \
touch /var/tmp/ok 2>&1 && echo VARTMP_WRITABLE || echo VARTMP_READONLY",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("TMP_WRITABLE"),
"expected /tmp to remain writable in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("VARTMP_WRITABLE"),
"expected /var/tmp to remain writable in whitelist mode, got: {stdout}"
);
}
#[test] #[test]
fn rw_missing_path_errors() { fn rw_missing_path_errors() {
let output = sandbox(&["--rw", "/nonexistent/xyz"]) let output = sandbox(&["--rw", "/nonexistent/xyz"])