danger-full-access sandbox blocks Metal/GPU IOKit access, crashing MLX on macOS

Open 💬 8 comments Opened Apr 13, 2026 by YuancFeng
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Environment

  • codex-cli 0.118.0
  • macOS 26.4 (25E246), Apple M4 Pro
  • Python 3.14.2, MLX 0.31.0
  • Config: sandbox = "danger-full-access", bypass-sandbox = true, CLI alias with --dangerously-bypass-approvals-and-sandbox

Description

When Codex spawns a Python subprocess that imports mlx.core (e.g., for local voice transcription via mlx-whisper), the process crashes with SIGABRT due to an uncaught NSRangeException:

*** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty array

The crash occurs in mlx::core::metal::Device::Device() during Metal GPU device enumeration. Metal's MTLCopyAllDevices() returns an empty array because the Seatbelt sandbox profile restricts IOKit access.

Root Cause

The Seatbelt profile compiled into the Codex binary only allows:

(allow iokit-open
  (iokit-registry-entry-class "RootDomainUserClient"))

This applies even in danger-full-access mode. Metal GPU enumeration requires IOKit access to GPU driver classes (e.g., AGXMetalG16X on Apple Silicon), which is not permitted.

Reproduction

Minimal reproduction without Codex, confirming the Seatbelt IOKit restriction is the root cause:

# This crashes with the identical NSRangeException:
sandbox-exec -p '(version 1)(allow default)(deny iokit-open)' \
  python3 -c "import mlx.core"

# This works fine (full IOKit access):
sandbox-exec -p '(version 1)(allow default)' \
  python3 -c "import mlx.core; print(mlx.core.default_device())"

Expected Behavior

danger-full-access mode (and --dangerously-bypass-approvals-and-sandbox) should grant unrestricted IOKit access, allowing Metal/GPU operations in child processes.

Suggested Fix

Add an unrestricted IOKit rule to the danger-full-access sandbox template:

(allow iokit-open)

Or, if a scoped approach is preferred:

(allow iokit-open
  (iokit-registry-entry-class "RootDomainUserClient")
  (iokit-registry-entry-class "AGXDeviceUserClient")
  (iokit-registry-entry-class "IOGPUDeviceUserClient"))

Crash Report (abbreviated)

Process:     Python
Parent:      codex
Exception:   EXC_CRASH (SIGABRT)
Reason:      *** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty array

Crash thread backtrace:
  CoreFoundation  -[__NSArray0 objectAtIndex:]
  libmlx.dylib    mlx::core::metal::Device::Device()
  libmlx.dylib    mlx::core::metal::device(mlx::core::Device)
  libmlx.dylib    mlx::core::metal::MetalAllocator::MetalAllocator()
  libmlx.dylib    mlx::core::allocator::allocator()
  libmlx.dylib    mlx::core::random::key(unsigned long long)
  core.cpython-314-darwin.so  [module init]

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • ##16931

Powered by Codex Action

YuancFeng · 3 months ago

Thanks for flagging. #16931 describes the same symptom (Metal invisible inside sandbox), but this issue adds the root cause analysis: the Seatbelt profile only allows iokit-open for RootDomainUserClient, which blocks GPU driver classes needed by Metal. I also include a minimal sandbox-exec reproduction and a concrete fix suggestion.

Happy to close this if #16931 gets the fix merged, but keeping it open for now since it has more actionable detail.

m13v · 3 months ago

the IOKit restriction goes deeper than just Metal device enumeration. CoreML model compilation (which WhisperKit, mlx-whisper, and most on-device inference frameworks trigger) also needs IOKit access to detect the Neural Engine and determine which execution providers to use. without it, the framework falls back to CPU-only inference, which is 5-10x slower than ANE on Apple Silicon.

we hit a related issue building local voice transcription. even after granting IOKit access, CoreML model compilation on first run spawns a background coremlcompiler process that itself needs IOKit and filesystem access to write the compiled .mlmodelc cache to the container's caches directory. if the sandbox restricts that write, you get a silent fallback to recompilation on every inference call, which pegs one core at 100% indefinitely.

the sandbox-exec reproduction is helpful. for anyone running into this before an upstream fix ships, you can pre-compile the MLX/CoreML model outside the sandbox and point the framework at the cached compiled version. this skips the IOKit-dependent compilation step entirely, though you still need the Metal device enumeration fix for actual GPU inference.

m13v · 3 months ago

our on-device transcription service that handles WhisperKit CoreML model compilation caching, the ANE vs CPU fallback logic, and the pre-compiled model path approach I mentioned: https://github.com/m13v/fazm/blob/main/Desktop/Sources/TranscriptionService.swift

viyatb-oai contributor · 3 months ago

@YuancFeng thanks for reporting this! if you are in danger-full-access and approval_policy=never, Codex should not execute code under seatbelt. It completely bypasses sandbox construction and seatbelt should not be invoked. Can you share your config.toml? do you have any other settings enforced via /etc/codex/requirements.toml?

YuancFeng · 3 months ago

@viyatb-oai Relevant config only:

approval_policy = "never"
bypass-approvals = true
bypass-sandbox = true
sandbox = "danger-full-access"

There is no /etc/codex/requirements.toml on this machine.

So the remaining question seems to be why a Codex-spawned Python child is still running under Seatbelt in this mode. If useful, I can provide:

  1. the exact Codex command or workflow step that spawns the failing child,
  2. sandbox denial or trace logs from that child,
  3. the full child crash report,
  4. a sysdiagnose privately rather than on the public issue.
viyatb-oai contributor · 3 months ago

please do! 1-3 should be enough.

YuancFeng · 3 months ago

@viyatb-oai Here are the three pieces you asked for, in minimal form. Happy to share the full IPS crash report, the complete turn_context, and the session rollout excerpt privately if any of it helps — just let me know your preferred channel (OpenAI email / internal ticket / etc.).

TL;DR

With config.toml setting sandbox = "danger-full-access" + approval_policy = "never" + bypass-sandbox = true + bypass-approvals = true, and the CLI launched via codex --dangerously-bypass-approvals-and-sandbox, the live session's turn_context.sandbox_policy.type still resolves to workspace-write, not danger-full-access. That is why the child process still ends up under Seatbelt and hits the IOKit restriction described in the issue body. I think the config path worth tracing is how the user's --dangerously-bypass-approvals-and-sandbox + sandbox = "danger-full-access" get translated into the in-session sandbox_policy — something in that path is downgrading it to workspace-write.

No /etc/codex/requirements.toml on this machine. No project-level AGENTS.md override of sandbox / approval settings either.

---

1. The command Codex issued inside the session

During a Codex TUI session working on a local project that wraps mlx_whisper, Codex wanted to introspect the mlx_whisper.transcribe API surface and issued an exec_command tool call along the lines of:

/bin/zsh -lc "python3 - <<'PY'
import mlx_whisper, inspect
print(inspect.signature(mlx_whisper.transcribe))
PY"

import mlx_whisper transitively imports mlx.core, which triggers MTLCopyAllDevices() during Metal device enumeration, which is where the crash happens.

The exec tool call returned exit code -1 with the NSRangeException / -[__NSArray0 objectAtIndex:] message already quoted in the issue body. In the session log, the turn_context recorded for that same turn shows approval_policy: "never" but sandbox_policy.type: "workspace-write" — that mismatch between the two is the actionable part.

---

2. Isolating the denial — minimal sandbox-exec repro

I can't paste raw kernel Seatbelt denial lines without sudo + the sandbox subsystem predicate, so the cleanest evidence I can give you is this deterministic A/B that isolates the trigger to exactly iokit-open:

# Denies iokit-open only. Crashes identically to the in-Codex failure.
sandbox-exec -p '(version 1)(allow default)(deny iokit-open)' \
  python3 -c "import mlx.core"
# → NSRangeException in mlx::core::metal::Device::Device(), SIGABRT

# Allows iokit-open. Runs fine.
sandbox-exec -p '(version 1)(allow default)' \
  python3 -c "import mlx.core; print(mlx.core.default_device())"
# → Device(gpu, 0)

The delta is one rule. This matches the (allow iokit-open (iokit-registry-entry-class "RootDomainUserClient"))-only pattern I pulled from the Codex binary.

If kernel-side deny(1) iokit-open lines (i.e. log show --predicate 'sender == "Sandbox"' around a reproduction) would help, I can capture those under sudo and share privately.

---

3. Crash — the frames that actually matter

Full IPS is available on request; here is the minimal stack that pinpoints the root cause. Relative offsets only, no base addresses, registers, or binary UUIDs — those don't add diagnostic value once symbols are present, and I'd rather not leave a full memory-layout fingerprint in a public thread.

Exception Type:   EXC_CRASH (SIGABRT)
Exception Reason: *** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty array

Thread 0 (com.apple.main-thread):
  CoreFoundation  -[__NSArray0 objectAtIndex:]             + 264
  libmlx.dylib    mlx::core::metal::Device::Device()       + 204
  libmlx.dylib    mlx::core::metal::device(Device)         +  80
  libmlx.dylib    mlx::core::metal::MetalAllocator::…      +  64
  libmlx.dylib    mlx::core::allocator::allocator()        +  80
  libmlx.dylib    mlx::core::array::init<…>(…)             +  64
  libmlx.dylib    mlx::core::array::array<…>(…)            + 152
  libmlx.dylib    mlx::core::random::key(unsigned long)    +  72
  core.cpython-*-darwin.so   [module init]
  Python          PyModule_ExecDef / _imp_exec_dynamic / PyImport_ImportModuleLevelObject / …

Reading the stack: MLX's metal::Device constructor assumes MTLCopyAllDevices() returns a non-empty NSArray. Under the Codex Seatbelt profile — which only allows iokit-open for the RootDomainUserClient class — the GPU driver's user-client class is denied, MTLCopyAllDevices() returns __NSArray0, and the immediate objectAtIndex:0 raises NSRangeException that the MLX side doesn't catch. The Metal driver bundle is loaded fine (it shows up in the image list), so this really is an IOKit-class policy problem, not a missing-driver problem.

---

Environment (coarse)

Apple Silicon Mac, macOS 26.x, stock homebrew Python 3.14, recent MLX release. The crash reproduces on the minimal python3 -c "import mlx.core" command above, so I don't think finer-grained versions are load-bearing — let me know if they are and I'll share the exact build info off-thread.