macOS sandbox: ProcessPoolExecutor fails — sysconf(SC_SEM_NSEMS_MAX) denied because kern.sysv.semmni is missing from the seatbelt sysctl-read allowlist
Summary
Any Python code using concurrent.futures.ProcessPoolExecutor fails inside the macOS sandbox with PermissionError: [Errno 1] Operation not permitted, even though every primitive a process pool actually needs is permitted.
The failure is not a capability limit — it's CPython's precondition check. ProcessPoolExecutor.__init__ calls _check_system_limits(), which calls os.sysconf("SC_SEM_NSEMS_MAX"). On macOS that reads the sysctl kern.sysv.semmni, which is not in the sandbox profile's (allow sysctl-read …) list.
This looks unintended: the profile already contains
; POSIX IPC - semaphores for Python multiprocessing
(allow ipc-posix-sem)
so Python multiprocessing is clearly meant to work — and it does. Only the pre-flight probe is blocked.
Environment
- Claude Code 2.1.219
- macOS 26.5.2 (arm64)
- Python 3.11.15
Reproduction
import os
from concurrent.futures import ProcessPoolExecutor
def main():
try:
print("sysconf(SC_SEM_NSEMS_MAX) =", os.sysconf("SC_SEM_NSEMS_MAX"))
except OSError as exc:
print("sysconf(SC_SEM_NSEMS_MAX) ->", type(exc).__name__, exc)
from _multiprocessing import SemLock
SemLock(1, 1, 1, f"/repro-{os.getpid()}", False)
print("named POSIX semaphore: OK")
try:
with ProcessPoolExecutor(max_workers=2) as pool:
print("ProcessPoolExecutor: OK, result =", sum(pool.map(abs, [-1, -2])))
except OSError as exc:
print("ProcessPoolExecutor ->", type(exc).__name__, exc)
if __name__ == "__main__":
main()
Inside the sandbox:
sysconf(SC_SEM_NSEMS_MAX) -> PermissionError [Errno 1] Operation not permitted
named POSIX semaphore: OK
ProcessPoolExecutor -> PermissionError [Errno 1] Operation not permitted
With the sandbox bypassed:
sysconf(SC_SEM_NSEMS_MAX) = 87381
named POSIX semaphore: OK
ProcessPoolExecutor: OK, result = 3
87381 matches sysctl -n kern.sysv.semmni on the same machine, confirming which sysctl SC_SEM_NSEMS_MAX reads.
Why it's only the probe
Everything a pool uses at runtime works in-sandbox. Verified individually:
| Primitive | In sandbox |
|---|---|
| _multiprocessing.SemLock (named POSIX semaphore) | OK |
| multiprocessing.Lock() | OK |
| multiprocessing.Queue() | OK |
| tempfile in TMPDIR | OK |
| os.sysconf("SC_NPROCESSORS_ONLN"), os.cpu_count() | OK (14) |
| os.sysconf("SC_SEM_NSEMS_MAX") | denied |
So the pool would run fine if it were allowed to start.
Suggested fix
Add kern.sysv.semmni to the (allow sysctl-read …) list in the macOS seatbelt profile. The list already carries ~17 kern.* entries (kern.argmax, kern.maxproc, kern.ngroups, …) but nothing under kern.sysv.. It's a read of a static system limit, so the additional exposure is negligible relative to what's already allowed.
Why a workaround isn't enough
The sandbox settings object accepts allowedHosts, allowUnixSockets, allowWithinDeny, denyWithinAllow, excludedCommands, network, and autoAllowBashIfSandboxed — none of which can extend the sysctl/syscall surface. So users can't fix this in configuration. The options today are:
- Refactor to single-process, where the code exposes a
--workers 1-style knob. - Bypass the sandbox for that invocation.
- Add the command to
sandbox.excludedCommands— which drops filesystem and network confinement for every matching command, a poor trade for regaining parallelism in a read-only script.
Option 3 in particular means the current behavior nudges users toward disabling the sandbox more broadly than they need to, which seems worth avoiding.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗