Merge pull request #7807 from chuiwenwei/fix/sandbox-available-probe

fix(sandbox): probe sandbox-exec usability instead of PATH presence
This commit is contained in:
SivanCola authored and GitHub committed 2026-08-07 19:13:18 +08:00
commit 82851c80be
3 files changed
+75 -5

No files matched your search

+2 -2
View File
@@ -82,7 +82,7 @@ func UnavailableRemediation() string {
case "linux":
return "Install bubblewrap (`bwrap`) or set [sandbox] bash = \"off\" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution."
case "darwin":
return "Ensure `sandbox-exec` is available on PATH or set [sandbox] bash = \"off\" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution."
return "Ensure `sandbox-exec` is installed and usable (the host must allow `sandbox_apply`), or set [sandbox] bash = \"off\" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution."
case "windows":
return "Windows does not currently provide a Reasonix OS-level Bash sandbox; the effective setting is fixed to \"off\" and shell commands run unconfined."
default:
@@ -97,7 +97,7 @@ func BackendUnavailableReason() string {
case "linux":
return "bubblewrap (bwrap) is unavailable on PATH"
case "darwin":
return "sandbox-exec is unavailable on PATH"
return "sandbox-exec is missing from PATH or unusable (sandbox_apply is restricted)"
case "windows":
return "the AppContainer helper or required Windows sandbox APIs are unavailable"
default:
+31 -3
View File
@@ -1,11 +1,14 @@
package sandbox
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
// Command returns the argv to run `command` through sh, wrapped in sandbox-exec
@@ -30,10 +33,35 @@ func CommandArgs(spec Spec, args []string) ([]string, bool) {
return append([]string{"sandbox-exec", "-p", seatbeltProfile(spec)}, args...), true
}
// Available reports whether sandbox-exec is on PATH (it ships with macOS).
// sandboxExecUsability caches the probe result per resolved binary path, so
// repeated Available() calls stay O(1) after the first check.
var sandboxExecUsability sync.Map // resolved executable path -> bool
// usableSandboxExec distinguishes an installed sandbox-exec from a usable
// Seatbelt backend. On restricted macOS hosts, sandbox-exec can be on PATH
// while sandbox_apply fails with exit 71. Probe that operation directly with a
// minimal profile, mirroring usableBwrap on Linux.
func usableSandboxExec() bool {
path, err := exec.LookPath("sandbox-exec")
if err != nil {
return false
}
if cached, ok := sandboxExecUsability.Load(path); ok {
return cached.(bool)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err = exec.CommandContext(ctx, path, "-p", "(version 1)(allow default)", "true").Run()
usable := err == nil
actual, _ := sandboxExecUsability.LoadOrStore(path, usable)
return actual.(bool)
}
// Available reports whether the OS sandbox backend can actually confine
// processes. macOS probes sandbox-exec; Linux verifies bubblewrap can enter its
// namespace (see seatbelt_other.go).
func Available() bool {
_, err := exec.LookPath("sandbox-exec")
return err == nil
return usableSandboxExec()
}
// seatbeltProfile builds an SBPL profile that allows everything, then denies
+42
View File
@@ -295,3 +295,45 @@ func TestGoBuildUnderSandbox(t *testing.T) {
t.Errorf("build output missing: %v", err)
}
}
// fakeSandboxExec writes an executable named sandbox-exec into a fresh temp
// dir, prepends that dir to PATH, and returns the binary's path. The probe
// resolves the binary via PATH, so the fake shadows any real sandbox-exec on
// the host.
func fakeSandboxExec(t *testing.T, exitCode string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "sandbox-exec")
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit "+exitCode+"\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
return path
}
// TestAvailableFalseWhenSandboxExecUnusable covers the macOS 10.14+ case where
// sandbox-exec is installed but sandbox_apply is refused (exit 71): the probe
// must report unavailable so enforce mode fails loudly at boot instead of
// silently on every command.
func TestAvailableFalseWhenSandboxExecUnusable(t *testing.T) {
path := fakeSandboxExec(t, "71")
sandboxExecUsability.Delete(path) // the fake is fresh per test; re-probe it
if Available() {
t.Fatal("Available() = true, want false: sandbox-exec on PATH but unusable (exit 71)")
}
}
func TestAvailableTrueWhenSandboxExecUsable(t *testing.T) {
path := fakeSandboxExec(t, "0")
sandboxExecUsability.Delete(path)
if !Available() {
t.Fatal("Available() = false, want true: working sandbox-exec")
}
}
func TestAvailableFalseWhenSandboxExecMissing(t *testing.T) {
t.Setenv("PATH", t.TempDir()) // no sandbox-exec anywhere on PATH
if Available() {
t.Fatal("Available() = true, want false: sandbox-exec not on PATH")
}
}