macOS sandbox: hw.optional.neon missing from sysctl allowlist — all Qt 6 apps abort at startup on Apple Silicon

Status Open
Maintainer reply None cached
Activity 1 comment · opened Jul 29, 2026

Summary

The macOS Seatbelt profile generated for the Bash sandbox allows three hw.optional.* sysctl prefixes but never hw.optional.neon, which matches none of them. Because the profile is (deny default ...), that read is denied.

Qt 6 reads hw.optional.neon from a library initializer, before main(). When the read fails, Qt concludes the CPU has no NEON, prints Incompatible processor, and calls qFatal()abort(). Every Qt 6 application aborts at startup when run inside the sandbox on Apple Silicon.

I hit this with OpenSCAD, but it is not OpenSCAD-specific — it affects any Qt 6 app invoked from a sandboxed Bash command (directly or via make, a test harness, a build script, etc.).

Root cause

From the profile builder in the CLI binary (v2.1.220):

; sysctl - specific sysctls only
(allow sysctl-read
  ...
  (sysctl-name-prefix "hw.optional.arm")
  (sysctl-name-prefix "hw.optional.arm.")
  (sysctl-name-prefix "hw.optional.armv8_")
  (sysctl-name-prefix "hw.perflevel")
  ...
)

Qt 6.8 reads exactly three CPU-feature sysctls. Two are covered, one is not:

| sysctl | matched by | result |
|---|---|---|
| hw.optional.armv8_crc32 | prefix hw.optional.armv8_ | allowed |
| hw.optional.arm.FEAT_AES | prefix hw.optional.arm. | allowed |
| hw.optional.neon | — (neon does not start with arm) | denied |

The three names are confirmed present in QtCore:

$ strings -a QtCore | grep -i 'hw\.optional'
hw.optional.arm.FEAT_AES
hw.optional.armv8_crc32
hw.optional.neon

Reproduction

Isolating the single variable — everything else allowed:

cat > /tmp/noneon.sb <<'EOF'
(version 1)
(allow default)
(deny sysctl-read (sysctl-name "hw.optional.neon"))
EOF

sandbox-exec -f /tmp/noneon.sb /Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD --info

Result:

Incompatible processor. This Qt build requires the following features:
    neon

Removing that one deny line makes it start normally. Any Qt 6 binary reproduces it.

Evidence from a real sandboxed run

Kernel log at the moment of the crash:

Sandbox: OpenSCAD(48915) deny(1) sysctl-read hw.optional.neon
AMFI: Denying core dump for pid 48915 (OpenSCAD)

hw.optional.neon was the only CPU-feature sysctl denied — armv8_crc32 was allowed by the existing prefix, exactly as predicted. The resulting crash report is EXC_CRASH (SIGABRT), single thread, ~25 ms after launch, with the faulting stack inside dyld4::prepare → findAndRunAllInitializers:

libsystem_c.dylib   abort
QtCore              qAbort()
QtCore              qDetectCpuFeatures.cold.1
QtCore              qDetectCpuFeatures + 468
dyld                dyld4::Loader::findAndRunAllInitializers(...)
dyld                dyld4::prepare(...)
dyld                start

Suggested fix

Add one entry to the (allow sysctl-read ...) block:

   (sysctl-name-prefix "hw.optional.arm.")
   (sysctl-name-prefix "hw.optional.armv8_")
+  (sysctl-name "hw.optional.neon")
   (sysctl-name-prefix "hw.perflevel")

Alternatively, broaden to (sysctl-name-prefix "hw.optional."), which would also cover hw.optional.floatingpoint, hw.optional.AdvSIMD_HPFPCvt, and similar legacy names that other toolchains probe. These are read-only booleans describing public CPU capabilities, so allowing the family carries little information-disclosure risk.

One caveat: I verified that denying hw.optional.neon causes the abort and that allowing it clears that specific failure. I could not fully verify the one-line change is sufficient end-to-end under the complete shipped profile, since I could not reconstruct it faithfully outside the CLI.

Workaround

Add the invoking command to sandbox.excludedCommands so it runs outside the sandbox.

Environment

  • Claude Code v2.1.220
  • macOS 26.5.2 (25F84), arm64 (Apple Silicon)
  • Qt 6.8.3 (as bundled in OpenSCAD 2026.06.12); reproducible with any Qt 6 binary

View original on GitHub ↗

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