Skip to content

fix: guard every HID verb against Device-Hub-poisoned simulators#61

Merged
onevcat merged 1 commit into
mainfrom
fix/device-hub-hid-guard
Jul 23, 2026
Merged

fix: guard every HID verb against Device-Hub-poisoned simulators#61
onevcat merged 1 commit into
mainfrom
fix/device-hub-hid-guard

Conversation

@onevcat

@onevcat onevcat commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #60 — a simulator booted while Device Hub is open silently drops all legacy HID (tap reported ✓ completed successfully with zero UI effect). Only type guarded against dtuhidd, and its presence-based predicate would have false-positived on healthy simulators if extended to tap. This PR replaces the predicate with one that can actually tell the two states apart, and moves the guard to the single choke point so every HID verb fails loudly.

The predicate

Live A/B on one device/runtime (see #60) established:

State dtuhidd present in the sim's domain HID works? dtuhidd start − launchd_sim start
Booted under Hub (poisoned) yes no — silent drop 1 s (spawned with the boot)
Hub attached after clean boot (benign) yes yes ≥ 34 s (even with a scripted immediate reopen)

Presence can't discriminate; start-time delta can. DeviceHubHIDSuppression (replacing KeyboardHIDSuppression) flags dtuhidd only when it is a child of the target's launchd_sim and started within a 15 s boot-attach window of it. The window leans generous deliberately: a false positive fails loudly with recovery steps + an escape hatch, a false negative would silently drop input. Implemented on LaunchdSimLocator's sysctl process table from #58 (ProcessRecord gains ppid; ~1–2 ms, no /bin/ps fork — the old guard's 40 ms spawn is gone as a bonus).

Changes

  • Guard wired into HIDInteractor.makeSession — covers tap/swipe/touch/gesture/multi-touch/button/key/type/batch; the redundant type-level check is removed (and its documented attach-after-boot false positive disappears).
  • Workaround message rewritten: names the boot-time rule, gives the reboot fix, and explains that SIM_USE_SKIP_DTUHIDD_CHECK=1 must reach the process that runs the check (combine with SIM_USE_NO_DAEMON=1, or sim-use daemon stop first) — discovered live when the override silently failed against a daemon spawned without it.
  • README / bundled skill prose updated (the pitfall is now a loud error, not a documented trap); work-record addendum in docs/ai/xxxx-xcode27-support/.

Tests

  • Tests/DeviceHubHIDSuppressionTests.swift — pure decision table over injected process tables: measured poisoned/benign rows, inclusive window boundary, other-simulator dtuhidd, fail-open cases, command-name forms, message contract. make test: 1124 green.
  • Live verification on Xcode 27 B4 (all four quadrants): poisoned boot → loud error through the daemon path ✅; escape hatch (SIM_USE_SKIP_DTUHIDD_CHECK=1 SIM_USE_NO_DAEMON=1) bypasses ✅; clean boot → taps land ✅; Hub attached after boot → taps land, no false positive ✅.

Superseded eventually by the idb DTUHID migration, same as the guard it replaces.

🤖 Generated with Claude Code

A simulator booted while Device Hub is open loses legacy HID delivery
(keyboard, and on CoreSimulator 1169.1+ touch as well): sends report
success while nothing lands. Only `type` guarded against this, and its
predicate — "dtuhidd currently running in the device's launchd_sim
domain" — could not tell that poisoned state apart from a Device Hub
attached to an already-booted simulator, where HID keeps working, so
extending it to `tap` would have blocked healthy simulators.

Live A/B on one device/runtime (issue #60) confirmed the boot-time
rule and calibrated a predicate that can tell the states apart:
process start times. Poisoned boots spawn dtuhidd together with
launchd_sim (measured 1 s apart); a later Hub attach spawns it when
the user opens the app (>= 34 s even scripted). The reworked guard —
DeviceHubHIDSuppression, replacing KeyboardHIDSuppression — flags
dtuhidd only when it is a child of the target's launchd_sim AND
started within a 15 s boot-attach window, reusing LaunchdSimLocator's
sysctl process table (ProcessRecord gains ppid; ~1-2 ms, no /bin/ps
fork).

The guard moves to HIDInteractor.makeSession, the single choke point
every legacy-HID verb passes through, so tap/swipe/touch/gesture/
multi-touch/button/key/type all fail loudly with recovery steps; the
type-level check is removed as redundant. The workaround message now
explains that SIM_USE_SKIP_DTUHIDD_CHECK=1 must reach the daemon
process (combine with SIM_USE_NO_DAEMON=1 or restart the daemon) —
found live when the override silently failed against a daemon spawned
without it.

Verified live on Xcode 27 B4: poisoned boot -> loud error through the
daemon path; clean boot -> taps land; Hub attached after boot -> taps
land, no false positive; escape hatch bypasses the guard.

Fixes #60

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Signed-off-by: onevcat <onevcat@gmail.com>
@onevcat
onevcat merged commit 2eeedf9 into main Jul 23, 2026
4 checks passed
@onevcat
onevcat deleted the fix/device-hub-hid-guard branch July 23, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tap family silently no-ops on Device-Hub-poisoned simulators — extend the dtuhidd guard to all HID verbs with a boot-attach criterion

1 participant