FARPSEC
Intelligence

0-Click Bluetooth Overflow in Apple's Root Daemon: Finding Two Memory Corruption Bugs in bluetoothd

On September 14, 2026, Apple shipped iOS 27, tvOS 27, watchOS 27, and visionOS 27. In the security advisories for all four releases, under CoreBluetooth - LE Additional Recognition:

> "We would like to acknowledge ... Maliq Barnard ... for their assistance."

This is the story behind that credit — two memory corruption bugs in Apple's Bluetooth daemon, found through systematic binary analysis and proven in the live root process.

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 _sprintf imports
  • 5 raw _strcpy imports
  • 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:

  1. Attaches to the live bluetoothd process (PID 413, uid=0)
  2. Calculates the ASLR slide from the Mach-O header
  3. Saves the original BSS contents
  4. Zeros buffer 1 and fills buffer 2 with a canary pattern ('B' × 256)
  5. Resolves raw _sprintf via dlsym(RTLD_DEFAULT, "sprintf") — bypassing fortification the same way the binary does
  6. Calls _sprintf inside the live daemon with a 256-character name
  7. 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

DateEvent
Aug 23, 2026Unsafe function scan flags bluetoothd (10 sprintf + 5 strcpy)
Aug 23, 2026BSS sprintf overflow identified at 0x10044ceac
Sep 2, 2026Deep binary RE confirms both findings; LLDB live verification
Sep 2, 2026Both submitted to Apple: OE110768377735 (BSS) + OE110768627827 (stack)
Sep 3, 2026Apple requests additional BLE delivery evidence; responded with XPC trigger + binary trace
Sep 14, 2026Apple ships fix across iOS 27, macOS Tahoe 26.7, tvOS 27, watchOS 27, visionOS 27
Sep 14, 2026Credited 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.