From 20b7718ce88fa18c8c6889cce0c0c9b88cd92972 Mon Sep 17 00:00:00 2001 From: JY Tan Date: Mon, 26 Jan 2026 14:30:54 -0800 Subject: [PATCH] fix: handle macOS /tmp symlink in sandbox allowWrite paths (#23) --- internal/sandbox/macos.go | 35 ++++++++++++++++++ internal/sandbox/macos_test.go | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/internal/sandbox/macos.go b/internal/sandbox/macos.go index 10f3b35..a093474 100644 --- a/internal/sandbox/macos.go +++ b/internal/sandbox/macos.go @@ -84,6 +84,38 @@ func getAncestorDirectories(pathStr string) []string { return ancestors } +// expandMacOSTmpPaths mirrors /tmp paths to /private/tmp equivalents and vice versa. +// On macOS, /tmp is a symlink to /private/tmp, and symlink resolution can fail if paths +// don't exist yet. Adding both variants ensures sandbox rules match kernel-resolved paths. +func expandMacOSTmpPaths(paths []string) []string { + seen := make(map[string]bool) + for _, p := range paths { + seen[p] = true + } + + var additions []string + for _, p := range paths { + var mirror string + switch { + case p == "/tmp": + mirror = "/private/tmp" + case p == "/private/tmp": + mirror = "/tmp" + case strings.HasPrefix(p, "/tmp/"): + mirror = "/private" + p + case strings.HasPrefix(p, "/private/tmp/"): + mirror = strings.TrimPrefix(p, "/private") + } + + if mirror != "" && !seen[mirror] { + seen[mirror] = true + additions = append(additions, mirror) + } + } + + return append(paths, additions...) +} + // getTmpdirParent gets the TMPDIR parent if it matches macOS pattern. func getTmpdirParent() []string { tmpdir := os.Getenv("TMPDIR") @@ -505,6 +537,9 @@ func WrapCommandMacOS(cfg *config.Config, command string, httpPort, socksPort in // Build allow paths: default + configured allowPaths := append(GetDefaultWritePaths(), cfg.Filesystem.AllowWrite...) + // Expand /tmp <-> /private/tmp for macOS symlink compatibility + allowPaths = expandMacOSTmpPaths(allowPaths) + // Enable local binding if ports are exposed or if explicitly configured allowLocalBinding := cfg.Network.AllowLocalBinding || len(exposedPorts) > 0 diff --git a/internal/sandbox/macos_test.go b/internal/sandbox/macos_test.go index ae019eb..6daf2bd 100644 --- a/internal/sandbox/macos_test.go +++ b/internal/sandbox/macos_test.go @@ -176,3 +176,70 @@ func TestMacOS_ProfileNetworkSection(t *testing.T) { }) } } + +// TestExpandMacOSTmpPaths verifies that /tmp and /private/tmp paths are properly mirrored. +func TestExpandMacOSTmpPaths(t *testing.T) { + tests := []struct { + name string + input []string + want []string + }{ + { + name: "mirrors /tmp to /private/tmp", + input: []string{".", "/tmp"}, + want: []string{".", "/tmp", "/private/tmp"}, + }, + { + name: "mirrors /private/tmp to /tmp", + input: []string{".", "/private/tmp"}, + want: []string{".", "/private/tmp", "/tmp"}, + }, + { + name: "no change when both present", + input: []string{".", "/tmp", "/private/tmp"}, + want: []string{".", "/tmp", "/private/tmp"}, + }, + { + name: "no change when neither present", + input: []string{".", "~/.cache"}, + want: []string{".", "~/.cache"}, + }, + { + name: "mirrors /tmp/fence to /private/tmp/fence", + input: []string{".", "/tmp/fence"}, + want: []string{".", "/tmp/fence", "/private/tmp/fence"}, + }, + { + name: "mirrors /private/tmp/fence to /tmp/fence", + input: []string{".", "/private/tmp/fence"}, + want: []string{".", "/private/tmp/fence", "/tmp/fence"}, + }, + { + name: "mirrors nested subdirectory", + input: []string{".", "/tmp/foo/bar"}, + want: []string{".", "/tmp/foo/bar", "/private/tmp/foo/bar"}, + }, + { + name: "no duplicate when mirror already present", + input: []string{".", "/tmp/fence", "/private/tmp/fence"}, + want: []string{".", "/tmp/fence", "/private/tmp/fence"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandMacOSTmpPaths(tt.input) + + if len(got) != len(tt.want) { + t.Errorf("expandMacOSTmpPaths() = %v, want %v", got, tt.want) + return + } + + for i, v := range got { + if v != tt.want[i] { + t.Errorf("expandMacOSTmpPaths()[%d] = %v, want %v", i, v, tt.want[i]) + } + } + }) + } +}