Skip to content

Commit 20b7718

Browse files
authored
fix: handle macOS /tmp symlink in sandbox allowWrite paths (#23)
1 parent 006d3b0 commit 20b7718

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

internal/sandbox/macos.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,38 @@ func getAncestorDirectories(pathStr string) []string {
8484
return ancestors
8585
}
8686

87+
// expandMacOSTmpPaths mirrors /tmp paths to /private/tmp equivalents and vice versa.
88+
// On macOS, /tmp is a symlink to /private/tmp, and symlink resolution can fail if paths
89+
// don't exist yet. Adding both variants ensures sandbox rules match kernel-resolved paths.
90+
func expandMacOSTmpPaths(paths []string) []string {
91+
seen := make(map[string]bool)
92+
for _, p := range paths {
93+
seen[p] = true
94+
}
95+
96+
var additions []string
97+
for _, p := range paths {
98+
var mirror string
99+
switch {
100+
case p == "/tmp":
101+
mirror = "/private/tmp"
102+
case p == "/private/tmp":
103+
mirror = "/tmp"
104+
case strings.HasPrefix(p, "/tmp/"):
105+
mirror = "/private" + p
106+
case strings.HasPrefix(p, "/private/tmp/"):
107+
mirror = strings.TrimPrefix(p, "/private")
108+
}
109+
110+
if mirror != "" && !seen[mirror] {
111+
seen[mirror] = true
112+
additions = append(additions, mirror)
113+
}
114+
}
115+
116+
return append(paths, additions...)
117+
}
118+
87119
// getTmpdirParent gets the TMPDIR parent if it matches macOS pattern.
88120
func getTmpdirParent() []string {
89121
tmpdir := os.Getenv("TMPDIR")
@@ -505,6 +537,9 @@ func WrapCommandMacOS(cfg *config.Config, command string, httpPort, socksPort in
505537
// Build allow paths: default + configured
506538
allowPaths := append(GetDefaultWritePaths(), cfg.Filesystem.AllowWrite...)
507539

540+
// Expand /tmp <-> /private/tmp for macOS symlink compatibility
541+
allowPaths = expandMacOSTmpPaths(allowPaths)
542+
508543
// Enable local binding if ports are exposed or if explicitly configured
509544
allowLocalBinding := cfg.Network.AllowLocalBinding || len(exposedPorts) > 0
510545

internal/sandbox/macos_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,70 @@ func TestMacOS_ProfileNetworkSection(t *testing.T) {
176176
})
177177
}
178178
}
179+
180+
// TestExpandMacOSTmpPaths verifies that /tmp and /private/tmp paths are properly mirrored.
181+
func TestExpandMacOSTmpPaths(t *testing.T) {
182+
tests := []struct {
183+
name string
184+
input []string
185+
want []string
186+
}{
187+
{
188+
name: "mirrors /tmp to /private/tmp",
189+
input: []string{".", "/tmp"},
190+
want: []string{".", "/tmp", "/private/tmp"},
191+
},
192+
{
193+
name: "mirrors /private/tmp to /tmp",
194+
input: []string{".", "/private/tmp"},
195+
want: []string{".", "/private/tmp", "/tmp"},
196+
},
197+
{
198+
name: "no change when both present",
199+
input: []string{".", "/tmp", "/private/tmp"},
200+
want: []string{".", "/tmp", "/private/tmp"},
201+
},
202+
{
203+
name: "no change when neither present",
204+
input: []string{".", "~/.cache"},
205+
want: []string{".", "~/.cache"},
206+
},
207+
{
208+
name: "mirrors /tmp/fence to /private/tmp/fence",
209+
input: []string{".", "/tmp/fence"},
210+
want: []string{".", "/tmp/fence", "/private/tmp/fence"},
211+
},
212+
{
213+
name: "mirrors /private/tmp/fence to /tmp/fence",
214+
input: []string{".", "/private/tmp/fence"},
215+
want: []string{".", "/private/tmp/fence", "/tmp/fence"},
216+
},
217+
{
218+
name: "mirrors nested subdirectory",
219+
input: []string{".", "/tmp/foo/bar"},
220+
want: []string{".", "/tmp/foo/bar", "/private/tmp/foo/bar"},
221+
},
222+
{
223+
name: "no duplicate when mirror already present",
224+
input: []string{".", "/tmp/fence", "/private/tmp/fence"},
225+
want: []string{".", "/tmp/fence", "/private/tmp/fence"},
226+
},
227+
}
228+
229+
for _, tt := range tests {
230+
t.Run(tt.name, func(t *testing.T) {
231+
got := expandMacOSTmpPaths(tt.input)
232+
233+
if len(got) != len(tt.want) {
234+
t.Errorf("expandMacOSTmpPaths() = %v, want %v", got, tt.want)
235+
return
236+
}
237+
238+
for i, v := range got {
239+
if v != tt.want[i] {
240+
t.Errorf("expandMacOSTmpPaths()[%d] = %v, want %v", i, v, tt.want[i])
241+
}
242+
}
243+
})
244+
}
245+
}

0 commit comments

Comments
 (0)