macOS sandbox: ProcessPoolExecutor fails — sysconf(SC_SEM_NSEMS_MAX) denied because kern.sysv.semmni is missing from the seatbelt sysctl-read allowlist

Status Open
Reported on v2.1.219
Maintainer reply None cached
Activity 1 comment · opened Jul 25, 2026

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:

  1. Refactor to single-process, where the code exposes a --workers 1-style knob.
  2. Bypass the sandbox for that invocation.
  3. 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗