返回 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 root := "/tmp/project/cache"
108 argv := bwrapBaseArgs(Spec{
109 Mode: "enforce",
110 WriteRoots: []string{root},
111 SessionTemp: "/private/session-tmp",
112 MinimalWrites: true,
113 })
114 tmpMount := indexArgs(argv, "--bind", "/private/session-tmp", "/tmp")
115 parent := indexArgs(argv, "--dir", "/tmp/project")
116 reopen := indexArgs(argv, "--bind", root, root)
117 if tmpMount < 0 || parent < tmpMount || reopen < parent {
118 t.Fatalf("temporary write root must be recreated after the private /tmp mount: %v", argv)
119 }
120 }
121
122 func TestBwrapProtectedWriteArgsIncludesMissingStateBoundary(t *testing.T) {
123 home := t.TempDir()
124 state := filepath.Join(home, "future-state")
125 argv := bwrapBaseArgs(Spec{
126 Mode: "enforce",
127 WriteRoots: []string{home},
128 ProtectedWriteRoots: ProtectedWriteRoots(state),
129 MinimalWrites: true,
130 })
131 if indexArgs(argv, "--ro-bind", state, state) < 0 {
132 t.Fatalf("missing protected state must fail closed at launch: %v", argv)
133 }
134 }
135
136 func TestBwrapProtectedWriteArgsSkipsUnreachableStateBoundary(t *testing.T) {
137 state := filepath.Join(t.TempDir(), "future-state")
138 argv := bwrapBaseArgs(Spec{
139 Mode: "enforce",
140 WriteRoots: []string{t.TempDir()},
141 ProtectedWriteRoots: ProtectedWriteRoots(state),
142 MinimalWrites: true,
143 })
144 if indexArgs(argv, "--ro-bind", state, state) >= 0 {
145 t.Fatalf("read-only filesystem already protects a disjoint state boundary: %v", argv)
146 }
147 }
148
149 func TestBwrapArgsBindsSessionTempAtTmp(t *testing.T) {
150 private := t.TempDir()
151 argv := bwrapArgs(Spec{
152 Mode: "enforce",
153 SessionTemp: private,
154 WriteRoots: []string{t.TempDir()},
155 }, Shell{Kind: ShellBash, Path: "bash"}, "true")
156 bind := indexArgs(argv, "--bind", private, "/tmp")
157 if bind < 0 {
158 t.Fatalf("expected --bind %s /tmp in %v", private, argv)
159 }
160 if indexArgs(argv, "--tmpfs", "/tmp") >= 0 {
161 t.Fatalf("session temp must not use tmpfs /tmp: %v", argv)
162 }
163 // Must not bind the host public temporary root as /tmp.
164 if host := os.TempDir(); host != private {
165 if indexArgs(argv, "--bind", host, "/tmp") >= 0 {
166 t.Fatalf("must not bind host temp %s at /tmp: %v", host, argv)
167 }
168 }
169 }
170
171 func TestBwrapArgsWithoutSessionTempKeepsTmpfs(t *testing.T) {
172 argv := bwrapArgs(Spec{Mode: "enforce"}, Shell{Kind: ShellBash, Path: "bash"}, "true")
173 if indexArgs(argv, "--tmpfs", "/tmp") < 0 {
174 t.Fatalf("independent sandbox should keep tmpfs /tmp: %v", argv)
175 }
176 }
177
178 func TestBwrapForbidReadArgsMasksFilesAndDirectories(t *testing.T) {
179 dir := t.TempDir()
180 nested := filepath.Join(dir, "nested")
181 if err := os.Mkdir(nested, 0o700); err != nil {
182 t.Fatal(err)
183 }
184 file := filepath.Join(t.TempDir(), "credentials.env")
185 if err := os.WriteFile(file, []byte("secret"), 0o600); err != nil {
186 t.Fatal(err)
187 }
188 missing := filepath.Join(dir, "missing")
189
190 got := bwrapForbidReadArgs([]string{dir, nested, file, file, missing})
191 want := []string{
192 "--tmpfs", dir,
193 "--ro-bind", "/dev/null", file,
194 }
195 if !reflect.DeepEqual(got, want) {
196 t.Fatalf("forbid-read mount args = %v, want %v", got, want)
197 }
198 }
199
200 func indexArgs(args []string, want ...string) int {
201 for i := 0; i+len(want) <= len(args); i++ {
202 if reflect.DeepEqual(args[i:i+len(want)], want) {
203 return i
204 }
205 }
206 return -1
207 }
208
209 func containsPath(paths []string, want string) bool {
210 absWant, err := filepath.Abs(want)
211 if err != nil {
212 return false
213 }
214 return slices.Contains(paths, absWant)
215 }
216
216 lines GO