Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/loopal-sandbox/src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::sync::OnceLock;
use loopal_config::{ResolvedPolicy, SandboxPolicy};

/// Static base policy loaded from the `.sbpl` file at compile time.
/// Contains: deny-default, process/sysctl/iokit/mach/ipc/pty rules,
/// system writable paths, and framework executable-mapping rules.
/// Contains: deny-default, broad system-access allows, IPC/PTY rules,
/// system writable paths, and unrestricted executable-mapping.
const BASE_POLICY: &str = include_str!("seatbelt_base.sbpl");

/// Cached result of the `sandbox-exec` availability probe.
Expand Down
89 changes: 18 additions & 71 deletions crates/loopal-sandbox/src/platform/seatbelt_base.sbpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
; Loopal macOS Seatbelt base policy
; Inspired by Codex / Chromium sandbox policy, with relaxations for broad
; CLI-tool compatibility.
;
; Security model: protect file-system writes (workspace containment) and
; sensitive-file access. Everything else — process execution, system-info
; reads, service lookups, device enumeration, dylib loading — is allowed
; because restricting them adds no security when process-exec/process-fork
; are already unrestricted, yet causes repeated tool-compatibility failures
; (Bazel TSan dylib, JVM crashes, Nix toolchains, etc.).
;
; This file is included via include_str!() and provides the static rules.
; Dynamic sections (file-read*, file-write*, network) are appended by Rust.
Expand All @@ -16,69 +21,19 @@
(allow signal (target same-sandbox))
(allow process-info* (target same-sandbox))

; --- sysctl ---
; Prefix-based whitelist: broader than Codex for tool compatibility,
; but still blocks writes (except the one JVM needs).
(allow sysctl-read
(sysctl-name-prefix "hw.")
(sysctl-name-prefix "kern.os")
(sysctl-name-prefix "kern.proc.")
(sysctl-name-prefix "machdep.cpu.")
(sysctl-name-prefix "net.routetable.")
(sysctl-name "kern.argmax")
(sysctl-name "kern.hostname")
(sysctl-name "kern.maxfilesperproc")
(sysctl-name "kern.maxproc")
(sysctl-name "kern.secure_kernel")
(sysctl-name "kern.usrstack64")
(sysctl-name "kern.version")
(sysctl-name "sysctl.proc_cputype")
(sysctl-name "vm.loadavg")
)
; --- System info (read-only, no security impact) ---
(allow sysctl-read)

; JVM passes a memory buffer to this sysctl; classified as "write" but
; conceptually it is a read.
; conceptually it is a read. All other sysctl writes stay denied.
(allow sysctl-write
(sysctl-name "kern.grade_cputype"))

; --- IOKit ---
; Only RootDomainUserClient (power/hardware info). This is enough for
; JVM, Node, Docker, and most native tools.
(allow iokit-open
(iokit-registry-entry-class "RootDomainUserClient"))
; --- IOKit (device enumeration, hardware info) ---
(allow iokit-open)

; --- Mach services ---
; Whitelist of system services needed by common dev tools.
(allow mach-lookup
; Directory / user info
(global-name "com.apple.system.opendirectoryd.libinfo")
(global-name "com.apple.system.opendirectoryd.membership")
(global-name "com.apple.system.DirectoryService.libinfo_v1")
(global-name "com.apple.bsd.dirhelper")
; Preferences
(global-name "com.apple.cfprefsd.agent")
(global-name "com.apple.cfprefsd.daemon")
(local-name "com.apple.cfprefsd.agent")
; Security / TLS
(global-name "com.apple.SecurityServer")
(global-name "com.apple.trustd")
(global-name "com.apple.trustd.agent")
(global-name "com.apple.ocspd")
(global-name "com.apple.secinitd")
; Network config
(global-name "com.apple.networkd")
(global-name "com.apple.SystemConfiguration.DNSConfiguration")
(global-name "com.apple.SystemConfiguration.configd")
; Logging / diagnostics
(global-name "com.apple.system.logger")
(global-name "com.apple.system.notification_center")
(global-name "com.apple.logd")
(global-name "com.apple.logd.events")
(global-name "com.apple.diagnosticd")
(global-name "com.apple.runningboard")
; Power management
(global-name "com.apple.PowerManagement.control")
)
; --- Mach services (XPC / system daemons) ---
(allow mach-lookup)

; --- IPC ---
; POSIX semaphores: needed by Python multiprocessing, database engines, etc.
Expand All @@ -101,15 +56,7 @@
(allow file-write* (subpath "/private/var/tmp"))

; --- Executable mapping ---
; Allow mmap(PROT_EXEC) for system frameworks and libraries so the
; dynamic linker can load .dylib files.
(allow file-map-executable
(subpath "/Library/Apple/System/Library/Frameworks")
(subpath "/Library/Apple/System/Library/PrivateFrameworks")
(subpath "/Library/Apple/usr/lib")
(subpath "/System/Library/Frameworks")
(subpath "/System/Library/PrivateFrameworks")
(subpath "/usr/lib")
(subpath "/opt/homebrew/lib")
(subpath "/usr/local/lib")
)
; Allow mmap(PROT_EXEC) everywhere — no security value when process-exec
; is unrestricted, but path-based whitelists break Bazel toolchains,
; Nix store, Homebrew Cellar, pyenv/rbenv, Cargo builds, etc.
(allow file-map-executable)
39 changes: 15 additions & 24 deletions crates/loopal-sandbox/tests/suite/platform_macos_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,25 @@ mod macos_tests {
assert!(profile.contains("(allow network*)"));
}

/// Non-file-system operations are broadly allowed because process-exec
/// is unrestricted — whitelisting them adds no security but breaks tools.
#[test]
fn process_rules_aligned_with_codex() {
fn system_access_rules_are_permissive() {
let profile = generate_seatbelt_profile(&workspace_policy());
// signal scoped to same-sandbox (not unrestricted)

// Process rules
assert!(profile.contains("(allow process-exec)"));
assert!(profile.contains("(allow process-fork)"));
assert!(profile.contains("(allow signal (target same-sandbox))"));
// process-info scoped to same-sandbox
assert!(profile.contains("(allow process-info* (target same-sandbox))"));
// iokit limited to RootDomainUserClient
assert!(profile.contains("(allow iokit-open"));
assert!(profile.contains("RootDomainUserClient"));
// sysctl-write only for JVM

// Blanket allows (no whitelists)
assert!(profile.contains("(allow sysctl-read)"));
assert!(profile.contains("(allow iokit-open)"));
assert!(profile.contains("(allow mach-lookup)"));
assert!(profile.contains("(allow file-map-executable)"));

// sysctl-write still restricted to JVM's kern.grade_cputype
assert!(profile.contains("kern.grade_cputype"));
}

Expand All @@ -92,21 +100,4 @@ mod macos_tests {
assert!(profile.contains("(allow pseudo-tty)"));
assert!(profile.contains("/dev/ptmx"));
}

#[test]
fn mach_lookup_is_whitelist() {
let profile = generate_seatbelt_profile(&workspace_policy());
// Must contain specific service names, not blanket mach-lookup
assert!(profile.contains("com.apple.system.opendirectoryd.libinfo"));
assert!(profile.contains("com.apple.trustd"));
assert!(profile.contains("com.apple.SystemConfiguration.DNSConfiguration"));
}

#[test]
fn file_map_executable_for_system_frameworks() {
let profile = generate_seatbelt_profile(&workspace_policy());
assert!(profile.contains("(allow file-map-executable"));
assert!(profile.contains("/System/Library/Frameworks"));
assert!(profile.contains("/usr/lib"));
}
}
Loading