返回 DeepSeek-Reasonix
seatbelt_other_test.go
根目录 / internal / sandbox / seatbelt_other_test.go
1 //go:build linux
2
3 package sandbox
4
5 import (
6 "os"
7 "path/filepath"
8 "reflect"
9 "slices"
10 "testing"
11 )
12
13 func TestLinuxWriteDirsSkipsMissingDirs(t *testing.T) {
14 home := t.TempDir()
15 t.Setenv("HOME", home)
16 if err := os.Mkdir(filepath.Join(home, ".cache"), 0o755); err != nil {
17 t.Fatal(err)
18 }
19
20 got := linuxWriteDirs()
21 if !containsPath(got, filepath.Join(home, ".cache")) {
22 t.Fatalf("existing cache dir missing from linux write dirs: %v", got)
23 }
24 for _, missing := range []string{".cargo", ".npm", "go"} {
25 if containsPath(got, filepath.Join(home, missing)) {
26 t.Fatalf("missing dir %s should not be bound: %v", missing, got)
27 }
28 }
29 }
30
31 func TestBwrapExecutableMountArgsRevealsOnlyExactTemporaryExecutable(t *testing.T) {
32 got := bwrapExecutableMountArgs([]string{"/tmp/go-build123/b456/plugin.test", "-test.run=Helper"})
33 want := []string{
34 "--dir", "/tmp/go-build123",
35 "--dir", "/tmp/go-build123/b456",
36 "--ro-bind", "/tmp/go-build123/b456/plugin.test", "/tmp/go-build123/b456/plugin.test",
37 }
38 if !reflect.DeepEqual(got, want) {
39 t.Fatalf("temporary executable mount args = %v, want %v", got, want)
40 }
41 }
42
43 func TestBwrapExecutableMountArgsLeavesVisibleExecutableAlone(t *testing.T) {
44 if got := bwrapExecutableMountArgs([]string{"/usr/bin/node", "server.js"}); got != nil {
45 t.Fatalf("visible executable mount args = %v, want nil", got)
46 }
47 }
48
49 func TestBwrapArgsForArgsMountsTemporaryExecutableAfterMasks(t *testing.T) {
50 secretDir := t.TempDir()
51 argv := bwrapArgsForArgs(Spec{
52 ForbidReadRoots: []string{secretDir},
53 }, []string{"/tmp/go-build123/b456/plugin.test", "-test.run=Helper"})
54 mask := indexArgs(argv, "--tmpfs", secretDir)
55 mount := indexArgs(argv, "--ro-bind", "/tmp/go-build123/b456/plugin.test", "/tmp/go-build123/b456/plugin.test")
56 if mask < 0 || mount < 0 || mount < mask {
57 t.Fatalf("temporary executable must be mounted after masks: %v", argv)
58 }
59 }
60
61 func TestBwrapProtectedWriteArgsRemountsReadonly(t *testing.T) {
62 home := t.TempDir()
63 state := filepath.Join(home, ".reasonix")
64 sessions := filepath.Join(state, "sessions")
65 if err := os.MkdirAll(sessions, 0o755); err != nil {
66 t.Fatal(err)
67 }
68 argv := bwrapBaseArgs(Spec{
69 Mode: "enforce",
70 WriteRoots: []string{home},
71 ProtectedWriteRoots: ProtectedWriteRoots(state),
72 MinimalWrites: true,
73 })
74 homeBind := indexArgs(argv, "--bind", home, home)
75 protect := indexArgs(argv, "--ro-bind", state, state)
76 if homeBind < 0 || protect < 0 || protect < homeBind {
77 t.Fatalf("protected root must be remounted read-only after the home bind: %v", argv)
78 }
79 }
80
81 func TestBwrapProtectedWriteArgsReallowsOnlySafeStateChild(t *testing.T) {
82 state := t.TempDir()
83 skills := filepath.Join(state, "skills")
84 projects := filepath.Join(state, "projects", "slug")
85 if err := os.MkdirAll(skills, 0o755); err != nil {
86 t.Fatal(err)
87 }
88 if err := os.MkdirAll(projects, 0o755); err != nil {
89 t.Fatal(err)
90 }
91 argv := bwrapBaseArgs(Spec{
92 Mode: "enforce",
93 WriteRoots: []string{skills, projects},
94 ProtectedWriteRoots: ProtectedWriteRoots(state),
95 MinimalWrites: true,
96 })
97 protect := indexArgs(argv, "--ro-bind", state, state)
98 if protect < 0 || indexArgs(argv[protect+1:], "--bind", skills, skills) < 0 {
99 t.Fatalf("safe state child must be reopened after parent protection: %v", argv)
100 }
101 if got := indexArgs(argv[protect+1:], "--bind", projects, projects); got >= 0 {
102 t.Fatalf("project runtime state must not be reopened: %v", argv)
103 }
104 }
105
106 func TestBwrapWriteRootUnderTmpReopensExactDirectory(t *testing.T) {
107 project, err := os.MkdirTemp("/tmp", "project-")
108 if err != nil {
109 t.Fatal(err)
110 }
111 t.Cleanup(func() { _ = os.RemoveAll(project) })
112 root := filepath.Join(project, "cache")
113 if err := os.Mkdir(root, 0o755); err != nil {
114 t.Fatal(err)
115 }
116 argv := bwrapBaseArgs(Spec{
117 Mode: "enforce",
118 WriteRoots: []string{root},
119 SessionTemp: "/private/session-tmp",
120 MinimalWrites: true,
121 })
122 tmpMount := indexArgs(argv, "--bind", "/private/session-tmp", "/tmp")
123 parent := indexArgs(argv, "--dir", project)
124 reopen := indexArgs(argv, "--bind", root, root)
125 if tmpMount < 0 || parent < tmpMount || reopen < parent {
126 t.Fatalf("temporary write root must be recreated after the private /tmp mount: %v", argv)
127 }
128 }
129
130 func TestBwrapProtectedWriteArgsIncludesMissingStateBoundary(t *testing.T) {
131 home := t.TempDir()
132 state := filepath.Join(home, "future-state")
133 argv := bwrapBaseArgs(Spec{
134 Mode: "enforce",
135 WriteRoots: []string{home},
136 ProtectedWriteRoots: ProtectedWriteRoots(state),
137 MinimalWrites: true,
138 })
139 if indexArgs(argv, "--ro-bind", state, state) < 0 {
140 t.Fatalf("missing protected state must fail closed at launch: %v", argv)
141 }
142 }
143
144 func TestBwrapProtectedWriteArgsSkipsUnreachableStateBoundary(t *testing.T) {
145 state := filepath.Join(t.TempDir(), "future-state")
146 argv := bwrapBaseArgs(Spec{
147 Mode: "enforce",
148 WriteRoots: []string{t.TempDir()},
149 ProtectedWriteRoots: ProtectedWriteRoots(state),
150 MinimalWrites: true,
151 })
152 if indexArgs(argv, "--ro-bind", state, state) >= 0 {
153 t.Fatalf("read-only filesystem already protects a disjoint state boundary: %v", argv)
154 }
155 }
156
157 func TestBwrapArgsBindsSessionTempAtTmp(t *testing.T) {
158 private := t.TempDir()
159 argv := bwrapArgs(Spec{
160 Mode: "enforce",
161 SessionTemp: private,
162 WriteRoots: []string{t.TempDir()},
163 }, Shell{Kind: ShellBash, Path: "bash"}, "true")
164 bind := indexArgs(argv, "--bind", private, "/tmp")
165 if bind < 0 {
166 t.Fatalf("expected --bind %s /tmp in %v", private, argv)
167 }
168 if indexArgs(argv, "--tmpfs", "/tmp") >= 0 {
169 t.Fatalf("session temp must not use tmpfs /tmp: %v", argv)
170 }
171 // Must not bind the host public temporary root as /tmp.
172 if host := os.TempDir(); host != private {
173 if indexArgs(argv, "--bind", host, "/tmp") >= 0 {
174 t.Fatalf("must not bind host temp %s at /tmp: %v", host, argv)
175 }
176 }
177 }
178
179 func TestBwrapArgsWithoutSessionTempKeepsTmpfs(t *testing.T) {
180 argv := bwrapArgs(Spec{Mode: "enforce"}, Shell{Kind: ShellBash, Path: "bash"}, "true")
181 if indexArgs(argv, "--tmpfs", "/tmp") < 0 {
182 t.Fatalf("independent sandbox should keep tmpfs /tmp: %v", argv)
183 }
184 }
185
186 func TestBwrapForbidReadArgsMasksFilesAndDirectories(t *testing.T) {
187 dir := t.TempDir()
188 nested := filepath.Join(dir, "nested")
189 if err := os.Mkdir(nested, 0o700); err != nil {
190 t.Fatal(err)
191 }
192 file := filepath.Join(t.TempDir(), "credentials.env")
193 if err := os.WriteFile(file, []byte("secret"), 0o600); err != nil {
194 t.Fatal(err)
195 }
196 missing := filepath.Join(dir, "missing")
197
198 got := bwrapForbidReadArgs([]string{dir, nested, file, file, missing})
199 want := []string{
200 "--tmpfs", dir,
201 "--ro-bind", "/dev/null", file,
202 }
203 if !reflect.DeepEqual(got, want) {
204 t.Fatalf("forbid-read mount args = %v, want %v", got, want)
205 }
206 }
207
208 func indexArgs(args []string, want ...string) int {
209 for i := 0; i+len(want) <= len(args); i++ {
210 if reflect.DeepEqual(args[i:i+len(want)], want) {
211 return i
212 }
213 }
214 return -1
215 }
216
217 func containsPath(paths []string, want string) bool {
218 absWant, err := filepath.Abs(want)
219 if err != nil {
220 return false
221 }
222 return slices.Contains(paths, absWant)
223 }
224
224 lines GO