block / block/buzz

macOS: crash on launch — EXC_GUARD (mach port INVALID_NAME) from double IOObjectRelease in user-idle

Open
#5,338 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

On macOS, `buzz-desktop` dies with `EXC_GUARD` / `GUARD_TYPE_MACH_PORT` within seconds of launch, every launch. The crash is inside the `user-idle` crate's macOS backend, which releases two IOKit objects twice per successful idle read. On most Macs this is silent; on any task whose exception-guard policy has `MP_FATAL` set, it is an immediate `SIGKILL`.

Reproduces on 0.5.5, 0.5.6, 0.5.7 and **0.5.8**.

## Crash

```
Exception Type: EXC_GUARD
Exception Subtype: GUARD_TYPE_MACH_PORT
Exception Message: INVALID_NAME on mach port (guarded with 0x0)

0 libsystem_kernel.dylib _kernelrpc_mach_port_deallocate_trap
1 libsystem_kernel.dylib mach_port_deallocate
2 buzz-desktop user_idle::UserIdle::get_time
3 buzz-desktop buzz_lib::run::{{closure}}::{{closure}}
4 buzz-desktop tauri::webview::Webview::on_message
```

Time-to-crash varies (observed 3s to 120s) because it is a race against mach port-name reuse, not a deterministic fault.

## Root cause

`desktop/src-tauri` depends on `user-idle = "0.6"`. In that crate's `src/macos_impl.rs`, the success path releases `iter` and `entry`, then falls through to the outer releases:

```rust
if iter > 0 {
entry = IOIteratorNext(iter);
if entry > 0 {
if prop_res == KERN_SUCCESS {
if present == 1 {
IOObjectRelease(iter); // release #1
IOObjectRelease(entry); // release #1
CFRelease(properties);
/* ...then reads `value`... */
}
}
}
IOObjectRelease(entry); // release #2 — always runs
}
IOObjectRelease(iter); // release #2 — always runs
```

`IOObjectRelease` on an `io_object_t` bottoms out in `mach_port_deallocate`, so every successful idle read over-releases both port names by one. The first extra release frees the name; the second refers to a freed name. If the kernel has already reused that number, an unrelated port is silently over-released and the app keeps running; if not, `INVALID_NAME` is raised.

Separately on the same path, `value` is borrowed from `properties` (a CF *Get*, not a *Copy*) but is read **after** `CFRelease(properties)` — a use-after-free.

## Why most users never see it

Whether the over-release is fatal depends on the task's `exc_guard` behavior, specifically `MP_FATAL` (`0x80`). Verified A/B on two machines with identical macOS 26.5.2, identical M4-class hardware, and the identical Buzz build:

| | machine A | machine B |
|---|---|---|
| `sysctl kern.task_exc_guard_default` | 153 | 153 |
| actual per-task guard | `0x00` | `0x99` (MP_FATAL set) |
| SIP | enabled | disabled |
| double-release result | `kr=15` (invalid name), ignored | **SIGKILL** |
| Buzz | never crashed | crashes every launch |

Both machines advertise the same sysctl default; only the SIP-disabled one applies it. So the bug is live everywhere — machine A is over-releasing a mach port on every idle poll right now — but only fails loudly where guards are fatal.

## Minimal reproduction (no Buzz involved)

```c
#include
#include
#include
int main(void) {
io_service_t svc = IOServiceGetMatchingService(kIOMainPortDefault,
IOServiceMatching("IOHIDSystem"));
printf("port name = %u\n", svc);
kern_return_t kr1 = mach_port_deallocate(mach_task_self(), svc);
printf("deallocate #1: kr=%d\n", kr1); fflush(stdout);
kern_return_t kr2 = mach_port_deallocate(mach_task_self(), svc);
printf("deallocate #2: kr=%d <-- SURVIVED\n", kr2);
return 0;
}
```

```
clang -x c - -o /tmp/pt -framework IOKit -framework CoreFoundation < repro.c && /tmp/pt
```

Prints `deallocate #2: kr=15` and exits 0 where guards are non-fatal; is killed with the same `EXC_GUARD` signature as Buzz where they are fatal. Clearing only `MP_FATAL` for the process (`task_set_exc_guard_behavior`) makes the identical double-release survive — and doing that to a running `buzz-desktop` stops it crashing, confirming the mechanism end to end.

## Suggested fix

The upstream crate looks unmaintained (last push Apr 2024). Options, roughly in order of effort:

1. Replace the call with `CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateHIDSystemState, kCGAnyInputEventType)` — one Apple API, no mach ports, no manual refcounting, removes the bug class.
2. Vendor/patch `user-idle`: return after the inner releases, and read `value` before `CFRelease(properties)` (or `CFRetain` it).

Happy to send a PR for either if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.