Skip to content

Revoking Accessibility at runtime freezes all system input (known issue) #2

Description

@PangMo5

Summary

Revoking Accessibility permission while Tatami is running freezes all macOS input (mouse clicks, keyboard, cmd+tab — everything), and recovery requires a reboot. The freeze is a known issue; the AX-revoke handling work was backed out (commit revert(input): back out the AX-revoke freeze work) rather than ship a fix that didn't hold.

Symptom

  • Toggle Tatami off under System Settings → Privacy & Security → Accessibility while it's running → the whole system stops responding to clicks/keys. Force-quit isn't reachable; a reboot is needed.
  • Distinctive clue: after the gesture tap was moved off the main run loop, the 4-finger workspace swipe still works during the freeze while everything else is dead — i.e. one tap's events still flow but the shared session event stream is otherwise wedged.

Mechanism (diagnosis)

Tatami installs several active CGEventTaps (kCGEventTapOptionDefault) on the shared session stream (.cgSessionEventTap):

Tap Events Always active?
FocusFollowsMouse mouseMoved yes (when FFM on)
MirrorClick leftMouseDown / rightMouseDown only when floating mirrors exist
BorrowChord keyDown only while a borrow direction-pick is armed
Gesture gesture when gestures on

An active tap is in-line in the HID delivery path. On AX revoke the taps lose authorization; left armed (and our tapDisabled handlers re-enabled them), they wedge the shared session stream — so clicks, keys and cmd+tab all freeze even though only mouseMoved/click events route through Tatami's taps. The always-on FocusFollowsMouse tap (mouseMoved) is the prime suspect; MirrorClick wedges clicks when mirrors are up.

What was confirmed via instrumentation

  • An off-main poll (cooperative pool, not the main actor) keeps running and logging during the freeze — so off-main work survives a blocked main thread.
  • AXIsProcessTrusted() does flip to false after a runtime revoke (heartbeat showed trusted=false).

What was tried (all failed)

  1. performFocus AXIsProcessTrusted() guard + accessibility.changes() teardown. The teardown runs on the main actor, which is already blocked during the freeze → it never executes.
  2. Move the gesture tap to EventTapThread. Swipe then survives the freeze, proving the mechanism, but clicks/keys still froze → the gesture tap wasn't the (only) wedger.
  3. Tear taps down on tapDisabled when !AXIsProcessTrusted(). Didn't engage — the system apparently doesn't reliably send tapDisabled on revoke, so the handler never fires; the taps stay armed-but-untrusted and keep wedging the stream.

Likely next direction (unvalidated)

  • An off-main poll (not tapDisabled-driven) that, on AXIsProcessTrusted() going false, proactively tears down all active session taps (FFM + MirrorClick at minimum), each teardown routed through EventTapThread so it runs even when main is blocked. Off-main detection + off-main teardown both confirmed feasible; the full teardown of MirrorClick (owned by the @MainActor FloatingOverlayController) needs an off-main-safe hook.
  • Alternatives to evaluate: make the observe-only taps listen-only (moves the TCC gate to Input Monitoring — the reason they're active today), and/or a periodic CGEventTapIsEnabled/CGEventTapCreate(...) == nil probe.

Workaround

Don't revoke Accessibility while Tatami is running. Re-granting requires a relaunch anyway, so quit Tatami first, then change the permission.

References

  • Apple — Quartz Event Services (active vs listen-only taps; kCGEventTapDisabledByTimeout; the callback must return promptly or the stream stalls).
  • Ghostty discussion #11390 (active .defaultTap on the main run loop → kernel holds all input until the tap watchdog fires).
  • Apple DevForums thread 735204 (AXIsProcessTrusted() staleness after revoke; clicks blocked).
  • yabai / skhd / Hammerspoon event-tap patterns (main-run-loop active taps survive only because their callbacks never block).

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions