Skip to content

ProcessPoolExecutor fails to construct when os.sysconf("SC_SEM_NSEMS_MAX") raises PermissionError #155912

Open

Description

@HarelTalasazanv2

Bug report

Bug description:

concurrent.futures.ProcessPoolExecutor cannot be constructed on macOS under
any sandbox profile that denies sysctl reads, even though every primitive it
depends on is fully functional in that environment.

_check_system_limits() in Lib/concurrent/futures/process.py reads the
semaphore limit like this:

try:
    nsems_max = os.sysconf("SC_SEM_NSEMS_MAX")
except (AttributeError, ValueError):
    # sysconf not available or setting not available
    return

The handler catches AttributeError and ValueError. It does not catch
OSError. On macOS, SC_SEM_NSEMS_MAX is backed by a sysctl, so a sandbox that
denies sysctl reads makes this call raise PermissionError (an OSError
subclass). The exception propagates out of ProcessPoolExecutor.__init__ before
any worker is created.

The inconsistency is with the function's own stated intent. The existing
except clause already treats "the limit cannot be determined" as benign and
returns — as does the nsems_max == -1 branch immediately below, commented
"indetermined limit, assume that limit is determined by available memory only".
A denied read is the same condition as an unavailable one, arriving as a
different exception type, but it is handled as a fatal error instead.

The practical result is that ProcessPoolExecutor becomes unavailable while
multiprocessing.Pool — same platform, same named semaphores, same spawn
machinery — works correctly. multiprocessing.Pool differs only in that it
never calls _check_system_limits.

This is a robustness bug, not a security issue. The sandbox behaves correctly by
denying the sysctl; the reproduction below denies it explicitly.

Reproduction

repro.py (attached) is standalone, has no third-party dependencies, and does
no monkeypatching. deny_sysctl.sb is a minimal profile that allows everything
except sysctl reads, so the outcome cannot be attributed to any other
restriction:

(version 1)
(allow default)
(deny sysctl-read)
$ python3 repro.py                                   # baseline
$ sandbox-exec -f deny_sysctl.sb python3 repro.py    # one capability denied

Actual behaviour

Under (deny sysctl-read) on macOS 26.5.2 (Darwin 25.5.0), CPython 3.13.12:

introspection (queries about a capability)
  os.sysconf(SC_SEM_NSEMS_MAX)     FAIL    PermissionError: [Errno 1] Operation not permitted (errno=1)
  _check_system_limits()           FAIL    PermissionError: [Errno 1] Operation not permitted (errno=1)

direct (exercises the capability)
  multiprocessing.Semaphore        OK      acquire/release
  multiprocessing.Process(spawn)   OK      spawn/join exit=0
  multiprocessing.Pool(spawn)      OK      [1, 4, 9, 16]
  concurrent.futures.ProcessPool   FAIL    PermissionError: [Errno 1] Operation not permitted (errno=1)

Traceback:

  File "concurrent/futures/process.py", line ..., in __init__
    _check_system_limits()
  File "concurrent/futures/process.py", line ..., in _check_system_limits
    nsems_max = os.sysconf("SC_SEM_NSEMS_MAX")
PermissionError: [Errno 1] Operation not permitted

Baseline run with no sandbox: all six checks pass.

Expected behaviour

ProcessPoolExecutor should construct and run. An unreadable semaphore limit is
already treated as benign when the read fails for other reasons; a denied read
should be treated the same way.

Suggested fix

Add OSError to the caught exceptions:

try:
    nsems_max = os.sysconf("SC_SEM_NSEMS_MAX")
except (AttributeError, ValueError, OSError):
    # sysconf not available, setting not available, or the read was denied
    # (e.g. a sandbox profile that denies sysctl reads on macOS)
    return

This preserves the genuine "too few semaphores" check, which raises
NotImplementedError with an explanatory message and is unaffected. It only
extends the existing "limit cannot be determined" path to cover denial.

If the narrower change is preferred, catching PermissionError alone fixes the
observed case, though any denied sysconf read raises some OSError and the
broader catch matches the comment's intent.

A regression test could patch os.sysconf to raise PermissionError and assert
that ProcessPoolExecutor still constructs — no sandbox required in CI.

Environment

  • macOS 26.5.2 (Darwin 25.5.0), arm64

Verified on three interpreters on the same host, all with identical results —
sysconf denied, ProcessPoolExecutor unconstructible, multiprocessing.Pool
working:

Interpreter Result
CPython 3.9.6 (/usr/bin/python3, Apple system Python) reproduces
CPython 3.12.13 reproduces
CPython 3.13.12 (python.org framework build) reproduces

The handler predates all three, so this is longstanding rather than a
regression. Note that the version shipped with macOS is affected.

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

macOS

Linked PRs

Activity

  1. HarelTalasazanv2 commented on Aug 16, 2026

    @HarelTalasazanv2
    Author

    Verified that the suggested fix resolves this, and that it doesn't weaken the check's actual purpose.

    Method: copied the stdlib concurrent package to a scratch directory, applied only the except change to the copy, and put it first on PYTHONPATH. The installed interpreter was not modified.

    The fix resolves it

    Same sandbox profile, same command — the only variable is which process.py is imported:

    $ sandbox-exec -f deny_sysctl.sb python3 repro.py
      multiprocessing.Pool(spawn)      OK      [1, 4, 9, 16]
      concurrent.futures.ProcessPool   FAIL    PermissionError: [Errno 1] Operation not permitted
    
    $ sandbox-exec -f deny_sysctl.sb env PYTHONPATH=<patched> python3 repro.py
      multiprocessing.Pool(spawn)      OK      [1, 4, 9, 16]
      concurrent.futures.ProcessPool   OK      [1, 4, 9, 16]

    The executor doesn't just construct — it returns correct results, so the semaphores were available throughout.

    The guard still fires

    _check_system_limits() driven directly against the patched module, resetting _system_limits_checked / _system_limited between cases:

    os.sysconf raises / returns Result
    PermissionError(1) returns (was fatal)
    OSError(5) returns (was fatal)
    ValueError returns (unchanged)
    -1 returns (unchanged)
    87381 returns (unchanged)
    10 NotImplementedError: system provides too few semaphores (10 available, 256 necessary)

    The last row is the one that matters: the change widens only the "cannot determine the limit" path.

    Regression test

    Five cases, patching os.sysconf rather than requiring a sandbox, so it runs everywhere CI does:

    $ python3 -m unittest test_addition            # unpatched
    Ran 5 tests ... FAILED (errors=2)
    
    $ PYTHONPATH=<patched> python3 -m unittest test_addition
    Ran 5 tests ... OK

    The two failures are exactly the PermissionError and OSError cases; the other three pass in both, so they aren't just restating the fix.

    I'm happy to open a PR with the change, the tests, and a NEWS entry if that's useful. On the scope question — OSError vs. just PermissionError — I went broad to match the existing comment's intent, but I'll narrow it if you'd prefer.

  2. picnixz commented on Aug 16, 2026

    @picnixz
    Member

    I'm happy to open a PR with the change, the tests, and a NEWS entry if that's useful. On the scope question — OSError vs. just PermissionError — I went broad to match the existing comment's intent, but I'll narrow it if you'd prefer.

    Do not use an LLM to generate replies directly please. We welcome contributions and it's not forbidden to use LLMs to understand the code base, but a human must be responsible (and should held so) for the code they propose.

    In addition, when you post issues in the future, edit the content so that we don't have a wall of text that makes it unreadable. TiA.


    Now, for the bug itself, we should actually catch OSError because the docs say:

    If name is a string and is not known, ValueError is raised. If a specific value for name is not supported by the host system, even if it is included in confstr_names, an OSError is raised with errno.EINVAL for the error number.

  3. HarelTalasazanv2 commented on Aug 23, 2026

    @HarelTalasazanv2
    Author
  4. picnixz commented on Aug 23, 2026

    @picnixz
    Member

    @HarelTalasazanv2 Do you want to suggest a PR for that?

  5. HarelTalasazanv2 commented on Aug 23, 2026

    @HarelTalasazanv2
    Author

    Yes, I will open the PR myself. Catching OS Error.

  6. added a commit that references this issue on Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    OS-macstdlibStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions