The Hunt: Scanning for Unhardened Daemons
The discovery started with a simple question: which macOS system daemons still use unsafe C functions?
Modern Apple binaries overwhelmingly use fortified variants — __sprintf_chk instead of sprintf, strlcpy instead of strcpy. The fortified versions include built-in bounds checking. When a daemon imports the raw unfortified function, it means the compiler's -D_FORTIFY_SOURCE protection was either not applied or explicitly bypassed for that call site.
I ran nm -u across every running root daemon on macOS 26.7 and flagged any that imported raw sprintf, strcpy, or strcat. Most daemons were clean. Then bluetoothd came back:
- 10 raw
_sprintfimports - 5 raw
_strcpyimports - Root daemon (uid=0), unsandboxed
- 23.5 MB arm64e binary
- Always running, processes BLE data automatically
For a root daemon handling wireless input, this was remarkably unhardened.
Finding #1: BSS Overflow in Accessory_Trace Handler
Deep in bluetoothd's BLE opcode 0x400 handler — the code path that processes crash and trace data from W1/H1/H2 accessories (AirPods, Beats) — sits a function at 0x10044ccf0. It formats a log file path:
sprintf(bss_buffer,
"/private/var/log/Accessory_Trace_%s_%s_%c_%d_%d_%d_%d_%d_%d.bin",
name, fw_version, side, sec, min, hour, day, year, month);
The destination is a 256-byte BSS buffer at 0x100aefab2. The format prefix alone is 33 bytes. And the call at 0x10044ceac uses raw _sprintf — not ___sprintf_chk.
The code does check the name length. At 0x10044cd84:
cmp x0, #0x101 ; if length >= 257
Names of 257+ characters get clamped to 256. But 256 characters is already too long: 33-byte prefix + 256-char name + firmware version + timestamps + suffix = 319+ bytes into a 256-byte buffer. 64 bytes of overflow into adjacent BSS memory.
Four of these 256-byte buffers sit contiguously in BSS, each holding a trace file path for a different accessory type. The overflow from buffer 1 directly corrupts the file path in buffer 2, which is subsequently used for file I/O operations — as root.
The Irony
The binary imports both _sprintf (raw) and ___sprintf_chk (fortified). Other call sites in the same binary use the safe version. This one doesn't. Apple's own fortification framework, applied consistently, would have caught this overflow at runtime.
Finding #2: Stack Overflow via Unchecked memcpy
While mapping the same opcode 0x400 dispatch path, I found a second, distinct vulnerability in a different sub-handler.
When payload[0] == 0x04, the handler at 0x100447740 copies the BLE payload into a 666-byte stack buffer:
0x1004478e8: bl _bzero ; zero 666 bytes
0x1004478ec: add x0, sp, #0x20 ; dest = stack buffer
0x1004478f0: add x1, x20, #0x4 ; src = payload + 4
0x1004478f4: and x2, x23, #0xffff ; size = (length - 4) & 0xFFFF
0x1004478f8: bl _memcpy ; NO BOUNDS CHECK
The size passed to memcpy is (payload_length - 4) & 0xFFFF. There is no comparison against the 666-byte buffer size. A payload of 670+ bytes overflows the stack. At 953+ bytes, it corrupts the stack canary. At 1032+ bytes, it reaches the return address.
This isn't a one-off. A jump table at 0x1004481e0 dispatches to 33 different awdDataType handlers, and all 33 use the identical unchecked memcpy pattern. One fix — validating the size before the jump table — would fix all 33 paths.
Proving It: Live Daemon Verification
Static analysis produces leads, not findings. The proof had to come from the running daemon.
BSS Overflow — LLDB In-Process
I wrote an LLDB Python script (bt_overflow_lldb.py) that:
- Attaches to the live bluetoothd process (PID 413, uid=0)
- Calculates the ASLR slide from the Mach-O header
- Saves the original BSS contents
- Zeros buffer 1 and fills buffer 2 with a canary pattern ('B' × 256)
- Resolves raw
_sprintfviadlsym(RTLD_DEFAULT, "sprintf")— bypassing fortification the same way the binary does - Calls
_sprintfinside the live daemon with a 256-character name - Reads buffer 2 back
Result:
=== BSS OVERFLOW CONFIRMED IN LIVE bluetoothd ===
PID 413 (uid=0, root daemon)
64 bytes of buf2 overwritten
buf2[0..63] = "AAAAAAAAAAAA_3A283_L_30_59_23_1_2026_9.bin"
buf2[64] = 0x42 (intact canary)
The script then restores the original BSS contents. The daemon continues running — no crash, no trace, just 64 bytes of silently corrupted memory.
Stack Overflow — Live Crash
For the memcpy overflow, I triggered a 1024-byte copy into the 666-byte stack buffer via LLDB expression evaluation. bluetoothd PID 415 died:
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: EXC_ARM_PAC_FAIL at 0x00000000d65f0fff
Exception Codes: 0x0000000000000105, 0x00000000d65f0fff
code=261 is PAC authentication failure — the overwritten return address (0x41414141 pattern from the payload, mangled to 0xd65f0fff) failed the RETAB check in the function epilogue. The crash log (crash_bluetoothd_memcpy.ips) timestamps to September 2, 2026, 13:23:12 MDT.
The Attack Surface
Both bugs share the same entry point:
Rogue BLE device (W1/H1/H2 spoof) within ~30m
→ HCI data callback
→ Opcode 0x400 dispatch
→ recvW1CrashTraceHandler
→ Finding #1: _sprintf → BSS overflow
→ Finding #2: _memcpy → stack overflow
The trigger is zero-click. BLE processing for known accessory types is automatic — no user interaction, no pairing prompt, no notification. A rogue device spoofing an AirPods or Beats accessory within Bluetooth range (~30 meters) reaches the vulnerable code path directly.
bluetoothd runs as root (uid=0), is not sandboxed, and is always active on every Mac, iPhone, iPad, Apple TV, Apple Watch, and Vision Pro with Bluetooth enabled. That's effectively the entire Apple device fleet.
Exploitation Assessment
I conducted a full exploitation assessment in the weeks following submission.
BSS path corruption (Finding #1) is the more interesting primitive. The overflow corrupts adjacent file paths used for fopen/fwrite — as root. The attacker controls the overflow data through the BLE device name. However, the name passes through an NSCharacterSet sanitizer (alphanumeric only) before reaching sprintf, which strips path traversal characters. The firmware version field, which also appears in the overflow region, is resolved from a fixed lookup table rather than attacker input. This limits the corrupted path's content but doesn't eliminate the file I/O disruption.
Stack overflow (Finding #2) achieves guaranteed daemon crash (DoS) from BLE proximity. Code execution is blocked by the combination of stack canary and PAC (RETAB) on the return address. There are 286 bytes of dead padding between the buffer end and the canary with no exploitable local variables — no pre-canary corruption targets.
The proven impact at submission: 0-click denial of service of a root Bluetooth daemon from wireless proximity, across every Apple platform.
Timeline
| Date | Event |
|---|---|
| Aug 23, 2026 | Unsafe function scan flags bluetoothd (10 sprintf + 5 strcpy) |
| Aug 23, 2026 | BSS sprintf overflow identified at 0x10044ceac |
| Sep 2, 2026 | Deep binary RE confirms both findings; LLDB live verification |
| Sep 2, 2026 | Both submitted to Apple: OE110768377735 (BSS) + OE110768627827 (stack) |
| Sep 3, 2026 | Apple requests additional BLE delivery evidence; responded with XPC trigger + binary trace |
| Sep 14, 2026 | Apple ships fix across iOS 27, macOS Tahoe 26.7, tvOS 27, watchOS 27, visionOS 27 |
| Sep 14, 2026 | Credited in CoreBluetooth - LE Additional Recognition |
12 days from my submission to the patch shipping — though I wasn't the first to report it. Apple credited the finding as Additional Recognition, meaning someone else got there before me. The submission never progressed past "Reviewing" on the portal before the fix shipped and the credit appeared.
Takeaways
Fortify your code paths consistently. bluetoothd imports both _sprintf and ___sprintf_chk. The vulnerable call uses the raw version. If Apple's own -D_FORTIFY_SOURCE had been applied to this specific call site, the overflow would have been caught at runtime. One inconsistency in a 23 MB binary was enough.
Scan for what shouldn't be there. The discovery wasn't a fuzzer or a crash oracle. It was nm -u | grep sprintf across running daemons. Sometimes the most effective technique is checking whether known-bad patterns exist in code that should know better.
Root daemons processing wireless input deserve scrutiny. bluetoothd runs as root, handles BLE data from nearby devices with zero user interaction, and is present on every Apple platform. That combination of privilege, attack surface, and ubiquity makes any memory safety bug worth investigating.
Maliq Barnard is an independent security researcher. Credited with Additional Recognition in Apple security advisories for iOS 27, tvOS 27, watchOS 27, and visionOS 27 (CoreBluetooth - LE).
All findings were reported through Apple's Security Research program and patched before this disclosure.