Skip to content

fix: never pin a HID transport auto-selected inside the boot-attach window - #72

Merged
onevcat merged 2 commits into
mainfrom
fix/hid-transport-trust-window
Jul 28, 2026
Merged

onevcat merged 2 commits into
mainfrom
fix/hid-transport-trust-window

Conversation

@onevcat

@onevcat onevcat commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #67.

Problem

Since the idb bump (#68), the HID transport is resolved by upstream's auto-selection at FBSimulatorHID construction: DTUHID when dtuhidd lives in the simulator's launchd_sim subtree, legacy Indigo otherwise. But dtuhidd attaches 0–3 s after boot on a simulator booted while Device Hub is open, so a HID verb landing inside that window probes the process tree too early, picks Indigo, and the cached connection pins the wrong transport for the entire boot — type exits 0 while delivering nothing, and since Indigo reports success, HIDPerformRecovery never fires. Real risk for simctl boot && sim-use type scripts and agents.

Fix

The issue's skip-the-cache variant, implemented as cache-but-don't-reuse so every connection stays on the explicit disconnect() path (a truly uncached connection would leak its mach port in the long-lived daemon):

  • New pure decision HIDBootIdentity.isTransportSelectionTrustworthy(token:now:) with transportTrustWindow = 15 s (the retired tap family silently no-ops on Device-Hub-poisoned simulators — extend the dtuhidd guard to all HID verbs with a boot-attach criterion #60 guard's window; launchd_sim's startedAt is already in the boot token, so no new probe).
  • HIDInteractor.CachedConnection carries transportTrusted; an entry created inside the window serves the command that created it but is discarded (disconnect + rebuild) by the next command, so the selection is re-derived once the attach window closes. Convergence: one command after the window.
  • A forced SIM_USE_HID_TRANSPORT selection cannot race and is cached unconditionally; an unknowable launchd_sim identity keeps the pre-existing marker-fallback caching behavior (the race evidence only exists where the probe works).
  • Steady-state path (uptime ≥ 15 s at first HID verb) is byte-for-byte unchanged.

Diagnostics prerequisite

SIM_USE_DEBUG=1 turns on the default logger's stderr sink (with debug-level detail), making the transport-selection signal lines visible in normal runs and — since the daemon redirects stderr to its logfile — in /tmp/sim-use-<uid>/<UDID>.log. Same daemon-environment caveat as SIM_USE_HID_TRANSPORT, noted in README.

Testing

  • TDD: the new HIDBootIdentity.isTransportSelectionTrustworthy suite was written against a stub reproducing today's always-cache behavior and failed on exactly the three new-behavior assertions (inside-window ×2, future start time) before the real implementation went in.
  • make build and make test green (1161 tests).
  • Live reproduction is limited: the poisoned-boot state did not reproduce headlessly during the HID transport selection races dtuhidd's boot-time attach; the cached connection pins the loser for the whole boot #67 investigation (appears to need Device Hub's device view attached), so behavior is locked by unit tests on the pure decision plus the trust-window log lines for field verification.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rir4WwrPShf5DYzziJ8RyC

…indow

dtuhidd attaches 0-3 s after boot on a simulator booted while Device
Hub is open, so upstream's transport auto-selection races it: a HID
verb landing inside the window picks Indigo, the connection is cached
under the boot token, and the wrong transport is silently pinned for
the entire boot (Indigo reports success without delivering, so
recovery never fires).

Gate the cache instead of the construction: a connection whose
transport was auto-selected while launchd_sim uptime is under 15 s is
served to the command that created it but never reused, so the next
command re-derives the selection after the attach window closes. This
is the issue's skip-the-cache variant, implemented as
cache-but-don't-reuse so every connection stays on the explicit
disconnect path. A forced SIM_USE_HID_TRANSPORT selection cannot race
and is cached unconditionally; an unknowable launchd_sim identity
keeps the marker-fallback caching behavior.

Also wire the diagnostics prerequisite: SIM_USE_DEBUG=1 turns on the
default logger's stderr sink so the transport-selection signals are
visible in normal runs and in the daemon logfile.

Fixes #67

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rir4WwrPShf5DYzziJ8RyC

Signed-off-by: onevcat <onevcat@gmail.com>
@onevcat

onevcat commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Live verification (Xcode 27 Beta 4, Device Hub)

Contrary to the "live reproduction is limited" caveat in the PR body, the race was reproduced live on this branch, and the fix held. With Device Hub open, simctl boot + immediate SIM_USE_NO_DAEMON=1 taps (round 1 of 3):

HID transport: auto (dtuhidd in this simulator's process tree: absent; ...)   ← probe raced ahead of the attach → Indigo picked
HID connection created ... inside the transport trust window (launchd_sim uptime < 15 s); the next command re-derives the transport selection
Dead HID transport ...; rebuilding session and retrying once...
HID transport: auto (dtuhidd in this simulator's process tree: present; ...)  ← dtuhidd attached meanwhile → DTUHID picked
HID connection created ... inside the transport trust window ...
✓ Tap at (200.0, 400.0) completed successfully

The losing selection was made, was not cached, and the very same command converged to DTUHID. (This round hit the loud dead-transport variant, which HIDPerformRecovery already handles; the silent type variant from #67 has no error to recover from and relies purely on the cache gate — which the log shows engaging on every inside-window construction.)

Daemon-mode chain also verified on a fresh boot (8 taps through the window, then 2 past it):
inside-window entries are created-but-not-reused and each next command logs "discarding it to re-derive the transport selection"; the first post-window command logs "created and cached" and the next one "Using existing HID connection" — one-command convergence, steady state unchanged.

Environment notes: on this machine dtuhidd attaches in <1 s (faster than the 0–3 s measured on 07-24), so only a cold in-process invocation races it; and Device Hub's device view did not re-attach across subsequent shutdown/boot cycles (rounds 2–3 stayed dtuhidd-free — consistent with the 07-24 observation that the poisoned state needs the device view attached; in that state legacy Indigo is genuinely functional and taps deliver).

Review follow-up: the env check lived only in SimUseLogger's no-arg
initializer, so `ios batch` - which passes its own flags via
SimUseLogger(writeToStdErr: verbose) - ignored SIM_USE_DEBUG entirely
and the transport-selection diagnostics stayed invisible without
--verbose.

OR-merge the env switch inside the parameterized convenience
initializer instead: one implementation covers every construction
path, batch needs no change, and --verbose keeps its exact behavior.
The env predicate takes an injectable environment for tests, which
lock the SIM_USE_NO_DAEMON-style "literal 1 only" convention.

Verified live: SIM_USE_DEBUG=1 sim-use ios batch --step "sleep 0.1"
(no --verbose) now emits the diagnostic info-lines on stderr; without
the env var stderr stays quiet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rir4WwrPShf5DYzziJ8RyC

Signed-off-by: onevcat <onevcat@gmail.com>
@onevcat
onevcat merged commit 50bc5a0 into main Jul 28, 2026
4 checks passed
@onevcat
onevcat deleted the fix/hid-transport-trust-window branch July 28, 2026 06:59
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.

HID transport selection races dtuhidd's boot-time attach; the cached connection pins the loser for the whole boot

1 participant