返回 DeepSeek-Reasonix
bash_sandbox_escape_test.go
根目录 / internal / tool / builtin / bash_sandbox_escape_test.go
1 package builtin
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "reasonix/internal/sandbox"
9 )
10
11 type fakeSandboxEscapeApprover struct {
12 allow bool
13 reason string
14 sessionAllowed bool
15 calls []sandbox.EscapeRequest
16 sessionChecks []sandbox.EscapeRequest
17 }
18
19 func (f *fakeSandboxEscapeApprover) ApproveSandboxEscape(ctx context.Context, req sandbox.EscapeRequest) (bool, string, error) {
20 f.calls = append(f.calls, req)
21 return f.allow, f.reason, nil
22 }
23
24 func (f *fakeSandboxEscapeApprover) SandboxEscapeSessionAllowed(ctx context.Context, req sandbox.EscapeRequest) bool {
25 f.sessionChecks = append(f.sessionChecks, req)
26 return f.sessionAllowed
27 }
28
29 func TestBashSandboxUnavailableFailsClosedEvenWithLegacyApprover(t *testing.T) {
30 if !sandbox.OSSandboxSupported() {
31 t.Skip("this host intentionally has no OS shell sandbox")
32 }
33 sh := sandbox.ResolveShell("", "", nil)
34 oldCommand := bashSandboxCommand
35 bashSandboxCommand = func(spec sandbox.Spec, sh sandbox.Shell, command string) ([]string, bool) {
36 return unconfinedShellArgv(sh, command), false
37 }
38 defer func() { bashSandboxCommand = oldCommand }()
39
40 approver := &fakeSandboxEscapeApprover{allow: true}
41 ctx := sandbox.WithEscapeApprover(context.Background(), approver)
42 args := argsJSON(t, map[string]any{"command": echoForShell(sh, "escaped")})
43 out, err := (bash{sb: sandbox.Spec{Mode: "enforce"}, shell: sh}).Execute(ctx, args)
44 if err == nil || !strings.Contains(err.Error(), "sandbox requested but unavailable") {
45 t.Fatalf("Execute = (%q, %v), want fail-closed sandbox error", out, err)
46 }
47 if len(approver.calls) != 0 {
48 t.Fatalf("legacy escape approver was called %d times", len(approver.calls))
49 }
50 }
51
52 func TestBashSandboxUnavailableStaysClosedWithoutApprover(t *testing.T) {
53 if !sandbox.OSSandboxSupported() {
54 t.Skip("this host intentionally has no OS shell sandbox")
55 }
56 sh := sandbox.ResolveShell("", "", nil)
57 oldCommand := bashSandboxCommand
58 bashSandboxCommand = func(spec sandbox.Spec, sh sandbox.Shell, command string) ([]string, bool) {
59 return unconfinedShellArgv(sh, command), false
60 }
61 defer func() { bashSandboxCommand = oldCommand }()
62
63 out, err := (bash{sb: sandbox.Spec{Mode: "enforce"}, shell: sh}).Execute(context.Background(), argsJSON(t, map[string]any{"command": echoForShell(sh, "escaped")}))
64 if err == nil {
65 t.Fatalf("Execute succeeded without escape approver, out=%q", out)
66 }
67 if !strings.Contains(err.Error(), "sandbox requested but unavailable") {
68 t.Fatalf("error = %v, want unavailable sandbox message", err)
69 }
70 }
71
72 func TestBashSandboxUnavailableDoesNotOpenLegacyDenialPrompt(t *testing.T) {
73 if !sandbox.OSSandboxSupported() {
74 t.Skip("this host intentionally has no OS shell sandbox")
75 }
76 sh := sandbox.ResolveShell("", "", nil)
77 oldCommand := bashSandboxCommand
78 bashSandboxCommand = func(spec sandbox.Spec, sh sandbox.Shell, command string) ([]string, bool) {
79 return unconfinedShellArgv(sh, command), false
80 }
81 defer func() { bashSandboxCommand = oldCommand }()
82
83 approver := &fakeSandboxEscapeApprover{allow: false, reason: "declined escape"}
84 ctx := sandbox.WithEscapeApprover(context.Background(), approver)
85 out, err := (bash{sb: sandbox.Spec{Mode: "enforce"}, shell: sh}).Execute(ctx, argsJSON(t, map[string]any{"command": echoForShell(sh, "escaped")}))
86 if err == nil {
87 t.Fatalf("Execute succeeded after denied escape, out=%q", out)
88 }
89 if !strings.Contains(err.Error(), "sandbox requested but unavailable") {
90 t.Fatalf("error = %v, want fail-closed sandbox reason", err)
91 }
92 if len(approver.calls) != 0 {
93 t.Fatalf("legacy escape approver was called %d times", len(approver.calls))
94 }
95 }
96
97 func TestBashUnsupportedOSSandboxUsesToolLayerPermissionBoundary(t *testing.T) {
98 if sandbox.OSSandboxSupported() {
99 t.Skip("this contract applies only to hosts without an OS shell sandbox")
100 }
101 sh := sandbox.ResolveShell("", "", nil)
102 oldCommand := bashSandboxCommand
103 called := false
104 bashSandboxCommand = func(spec sandbox.Spec, sh sandbox.Shell, command string) ([]string, bool) {
105 called = true
106 if spec.Enforce() {
107 t.Fatal("unsupported host passed an enforced spec to the retired sandbox backend")
108 }
109 return unconfinedShellArgv(sh, command), false
110 }
111 defer func() { bashSandboxCommand = oldCommand }()
112
113 approver := &fakeSandboxEscapeApprover{allow: true, sessionAllowed: true}
114 ctx := sandbox.WithPermissionPreset(sandbox.WithEscapeApprover(t.Context(), approver), "workspace-write")
115 out, err := (bash{sb: sandbox.Spec{Mode: "enforce"}, shell: sh}).Execute(ctx, argsJSON(t, map[string]any{"command": echoForShell(sh, "tool-layer-boundary"), "description": "verify unsupported sandbox contract"}))
116 if err != nil || !strings.Contains(out, "tool-layer-boundary") {
117 t.Fatalf("Execute = (%q, %v), want normal tool-layer execution", out, err)
118 }
119 if !called {
120 t.Fatal("shell command was not launched")
121 }
122 if len(approver.calls) != 0 || len(approver.sessionChecks) != 0 {
123 t.Fatalf("legacy sandbox approver was consulted: approvals=%d sessionChecks=%d", len(approver.calls), len(approver.sessionChecks))
124 }
125 }
126
127 func TestBashLegacySessionEscapeCannotBypassForegroundSandbox(t *testing.T) {
128 requirePOSIXShellTest(t)
129 sh := sandbox.ResolveShell("", "", nil)
130 oldCommand := bashSandboxCommand
131 bashSandboxCommand = func(spec sandbox.Spec, sh sandbox.Shell, command string) ([]string, bool) {
132 if spec.Enforce() {
133 return unconfinedShellArgv(sh, windowsSandboxFailureForShell(sh)), true
134 }
135 return unconfinedShellArgv(sh, command), false
136 }
137 defer func() { bashSandboxCommand = oldCommand }()
138 approver := &fakeSandboxEscapeApprover{sessionAllowed: true}
139 ctx := sandbox.WithEscapeApprover(context.Background(), approver)
140 out, err := (bash{sb: sandbox.Spec{Mode: "enforce"}, shell: sh}).Execute(ctx, argsJSON(t, map[string]any{"command": echoForShell(sh, "session-rerun")}))
141 if err == nil || !strings.Contains(out, "windows sandbox: boom") {
142 t.Fatalf("Execute = (%q, %v), want enforced sandbox failure", out, err)
143 }
144 if len(approver.calls) != 0 {
145 t.Fatalf("fresh approval calls = %d, want 0", len(approver.calls))
146 }
147 if len(approver.sessionChecks) != 0 {
148 t.Fatalf("legacy session grant was consulted %d times", len(approver.sessionChecks))
149 }
150 }
151
152 func echoForShell(sh sandbox.Shell, text string) string {
153 if sh.Kind == sandbox.ShellPowerShell {
154 return "Write-Output " + text
155 }
156 return "printf " + text
157 }
158
159 func windowsSandboxFailureForShell(sh sandbox.Shell) string {
160 if sh.Kind == sandbox.ShellPowerShell {
161 return "Write-Error 'windows sandbox: boom'; exit 126"
162 }
163 return "printf 'windows sandbox: boom\\n' >&2; exit 126"
164 }
165
165 lines GO