detached c0a7154

Allow null device in jailed bash commands

commit
c0a71542b7587f4e7174aa3530039f896d76b854
author
patriceckhart · · 2026-07-23 12:27 UTC
parent
734bf87
signature
unsigned
2 files · +29 -0
M packages/agent/tools/sandbox.go +6 -0
@@ -249,6 +249,12 @@ func looksLikePath(s string) bool {
249249 }
250250
251251 func (s *Sandbox) checkCommandPath(path string) error {
252+ // The platform null device is safe for shell input/output redirection even
253+ // though it lives outside the sandbox root. Keep this allowlist exact so
254+ // other device paths and lookalike descendants still fail closed.
255+ if path == os.DevNull {
256+ return nil
257+ }
252258 rootAbs, err := canonical(s.Root)
253259 if err != nil {
254260 return fmt.Errorf("sandbox root: %w", err)
@@ -249,6 +249,12 @@ func looksLikePath(s string) bool {
249 }249 }
250 250
251 func (s *Sandbox) checkCommandPath(path string) error {251 func (s *Sandbox) checkCommandPath(path string) error {
252+ // The platform null device is safe for shell input/output redirection even
253+ // though it lives outside the sandbox root. Keep this allowlist exact so
254+ // other device paths and lookalike descendants still fail closed.
255+ if path == os.DevNull {
256+ return nil
257+ }
252 rootAbs, err := canonical(s.Root)258 rootAbs, err := canonical(s.Root)
253 if err != nil {259 if err != nil {
254 return fmt.Errorf("sandbox root: %w", err)260 return fmt.Errorf("sandbox root: %w", err)
M packages/agent/tools/sandbox_test.go +23 -0
@@ -75,6 +75,29 @@ func TestBashToolRejectsOutsidePathWhenLocked(t *testing.T) {
7575 }
7676 }
7777
78+func TestSandboxAllowsNullDeviceRedirectionWhenLocked(t *testing.T) {
79+ sb := NewSandbox(t.TempDir())
80+ sb.Lock()
81+
82+ allowed := []string{
83+ "command -v go >" + os.DevNull + " 2>&1",
84+ "cat <" + os.DevNull,
85+ "cat \"" + os.DevNull + "\"",
86+ }
87+ for _, command := range allowed {
88+ if err := sb.CheckCommand(command); err != nil {
89+ t.Errorf("expected %q to be allowed: %v", command, err)
90+ }
91+ }
92+
93+ blocked := []string{"cat /dev/zero", "cat /dev/null/child"}
94+ for _, command := range blocked {
95+ if err := sb.CheckCommand(command); err == nil {
96+ t.Errorf("expected null-device lookalike %q to be blocked", command)
97+ }
98+ }
99+}
100+
78101 // TestSandboxAllowsCDIntoSubdir is the regression for issue #39: a `cd`
79102 // into a subdirectory of the sandbox root, spelled as an absolute path,
80103 // must be allowed. The old guard rejected any `cd /...` outright, which
@@ -75,6 +75,29 @@ func TestBashToolRejectsOutsidePathWhenLocked(t *testing.T) {
75 }75 }
76 }76 }
77 77
78+func TestSandboxAllowsNullDeviceRedirectionWhenLocked(t *testing.T) {
79+ sb := NewSandbox(t.TempDir())
80+ sb.Lock()
81+
82+ allowed := []string{
83+ "command -v go >" + os.DevNull + " 2>&1",
84+ "cat <" + os.DevNull,
85+ "cat \"" + os.DevNull + "\"",
86+ }
87+ for _, command := range allowed {
88+ if err := sb.CheckCommand(command); err != nil {
89+ t.Errorf("expected %q to be allowed: %v", command, err)
90+ }
91+ }
92+
93+ blocked := []string{"cat /dev/zero", "cat /dev/null/child"}
94+ for _, command := range blocked {
95+ if err := sb.CheckCommand(command); err == nil {
96+ t.Errorf("expected null-device lookalike %q to be blocked", command)
97+ }
98+ }
99+}
100+
78 // TestSandboxAllowsCDIntoSubdir is the regression for issue #39: a `cd`101 // TestSandboxAllowsCDIntoSubdir is the regression for issue #39: a `cd`
79 // into a subdirectory of the sandbox root, spelled as an absolute path,102 // into a subdirectory of the sandbox root, spelled as an absolute path,
80 // must be allowed. The old guard rejected any `cd /...` outright, which103 // must be allowed. The old guard rejected any `cd /...` outright, which

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close