From 28bb706c6155caf3b744f3da1a45c28fb76d6ae7 Mon Sep 17 00:00:00 2001 From: Blomios Date: Mon, 14 Sep 2026 09:25:34 +0200 Subject: [PATCH] =?UTF-8?q?fix(application,infrastructure):=20grant=20/dev?= =?UTF-8?q?/null=20RO|RW=20in=20workspace=5Fexec=5Fsandbox=20so=20CLIs=20c?= =?UTF-8?q?an=20open=20it=20O=5FRDWR=20=E2=80=94=20#354=20(QA=20verte)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cause: the workspace.execTool sandbox only granted the project root plus read-only system roots, so CLI tools like git failed when opening /dev/null with O_RDWR (redirect/exec 3<>/dev/null) inside sandboxed runs. Fix: workspace_exec_sandbox pushes one precise /dev/null PathGrant with RO|RW access; project read effect stays RO|EXEC and no blanket /dev grant is added, so the widening is limited to the null device; GitPlugin untouched. Tests: application asserts the read sandbox keeps project RO|EXEC while granting /dev/null RO|RW (workspace_exec_read_sandbox_grants_dev_null_without_widening_project_access); infrastructure proves the local runner executes exec 3<>/dev/null (local_runner_allows_cli_to_open_dev_null_for_read_and_write). QA verte reconfirmée par Git: cargo test -p application workspace_exec 5/5, -p infrastructure local_runner_allows_cli_to_open_dev_null 1/1, -p infrastructure workspace_tool::tests 5/5, cargo fmt --all --check, git diff --check. Co-Authored-By: Claude Opus 4.8 --- crates/application/src/plugin/mod.rs | 27 +++++++++++++++++++++ crates/infrastructure/src/workspace_tool.rs | 14 +++++++++++ 2 files changed, 41 insertions(+) diff --git a/crates/application/src/plugin/mod.rs b/crates/application/src/plugin/mod.rs index 7cc52c91..18b01136 100644 --- a/crates/application/src/plugin/mod.rs +++ b/crates/application/src/plugin/mod.rs @@ -2443,6 +2443,10 @@ fn workspace_exec_sandbox( abs_root: root.as_str().to_owned(), access: project_access, }]; + allowed.push(domain::PathGrant { + abs_root: "/dev/null".to_owned(), + access: domain::PathAccess::RO | domain::PathAccess::RW, + }); for system_root in ["/bin", "/usr", "/lib", "/lib64", "/etc"] { allowed.push(domain::PathGrant { abs_root: system_root.to_owned(), @@ -6063,6 +6067,29 @@ mod tests { } } + #[test] + fn workspace_exec_read_sandbox_grants_dev_null_without_widening_project_access() { + let root = ProjectPath::new("/workspace/example").unwrap(); + let sandbox = workspace_exec_sandbox(&root, PluginWorkspaceExecEffect::Read); + + assert_eq!( + sandbox + .allowed + .iter() + .find(|grant| grant.abs_root == root.as_str()) + .map(|grant| grant.access), + Some(domain::PathAccess::RO | domain::PathAccess::EXEC) + ); + assert_eq!( + sandbox + .allowed + .iter() + .find(|grant| grant.abs_root == "/dev/null") + .map(|grant| grant.access), + Some(domain::PathAccess::RO | domain::PathAccess::RW) + ); + } + #[tokio::test] async fn workspace_exec_allows_matching_rule_clamps_limits_and_audits_metadata() { let (project_id, tool, runner, audit) = workspace_exec_fixture(); diff --git a/crates/infrastructure/src/workspace_tool.rs b/crates/infrastructure/src/workspace_tool.rs index 0f0d3c25..dbb5ccc6 100644 --- a/crates/infrastructure/src/workspace_tool.rs +++ b/crates/infrastructure/src/workspace_tool.rs @@ -263,4 +263,18 @@ mod tests { assert!(result.stdout_truncated); assert!(!result.stderr_truncated); } + + #[cfg(unix)] + #[tokio::test] + async fn local_runner_allows_cli_to_open_dev_null_for_read_and_write() { + let runner = LocalWorkspaceToolRunner::new(Arc::new(crate::sandbox::NoopSandbox)); + let result = runner + .run(spec(&["-c", "exec 3<>/dev/null; printf ok"], 1_000, 32)) + .await + .unwrap(); + + assert_eq!(result.exit_code, Some(0)); + assert_eq!(result.stdout, b"ok"); + assert!(result.stderr.is_empty()); + } }