Skip to content

fix: gate HID connection reuse on launchd_sim process identity#58

Merged
onevcat merged 2 commits into
mainfrom
fix/hid-boot-identity-process-token
Jul 23, 2026
Merged

fix: gate HID connection reuse on launchd_sim process identity#58
onevcat merged 2 commits into
mainfrom
fix/hid-boot-identity-process-token

Conversation

@onevcat

@onevcat onevcat commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #55 — after an out-of-band simctl shutdown && boot, the surviving per-UDID daemon kept sending HID events through the previous boot's dead mach port: tap reported success while delivering nothing, the worst failure mode for an agent-facing tool.

Root cause

The existing boot-identity gate (HIDBootIdentity, #23-era) keyed cache reuse solely on the mtime of <dataDirectory>/var/run/launchd_bootstrap.plist, assuming CoreSimulator rewrites it on every boot. The investigation showed that assumption is conditional: forensics on the 2026-07-22 repro device found the marker frozen at 17:46:09 while var/run siblings showed boot-cycle activity through 19:20:39 (Xcode 27 B4 / CoreSimulator 1169.1; a CoreSimulatorService restart at 20:57 that evening suggests a mixed-version service state during the repro window). Equal mtimes across different boots ⇒ the dead connection was judged reusable. Notably, a clean-state re-run of the exact repro steps on both Xcode 27 B4 and an iOS 26.5 runtime did not reproduce — the marker advanced and the gate rebuilt correctly — which is exactly why the fix moves off the marker as the primary signal.

Fix

  • LaunchdSimLocator (new) — resolves the launchd_sim process serving a UDID via sysctl(KERN_PROC_ALL) + KERN_PROCARGS2 (~1–2 ms per probe; the /bin/ps pattern used elsewhere measures ~40 ms). launchd_sim's lifetime IS the boot; its (pid, kernel start time) is an unforgeable boot identity, immune to marker-rewrite semantics and pid reuse.
  • HIDBootIdentity — token becomes compound. Decision table: process identities on both sides → reuse iff equal (authoritative, the iOS daemon holds a stale HID connection across simulator reboot — tap reports success but is not delivered #55 fix); one-sided → fail closed; neither → previous marker-mtime rule as fallback.
  • HIDSendDeadline (new) — defence in depth for the residual window (reboot mid-command, or an unknown future token failure): each send runs under a 30 s deadline (SIM_USE_HID_SEND_TIMEOUT_MS, 0 disables) so a dead-port hang (the Xcode 26.5-observed mode) fails loudly instead of wedging the daemon. The timeout wording deliberately matches none of HIDPerformRecovery's dead-transport markers, so a timed-out composite is never blind-retried — pinned by a unit test.

The durable logic (token + reuse decision + locator) is pure and transport-agnostic; the planned idb/DTUHID migration replaces HIDInteractor and re-wires the same gate in one line.

Tests

  • HIDBootIdentityTests — compound-token decision table, including the iOS daemon holds a stale HID connection across simulator reboot — tap reports success but is not delivered #55 regression row (marker equal, process identity changed → reject).
  • LaunchdSimLocatorTests — pure matching on fixture tables (exact-comm filter, per-UDID argv scoping, overlapping-boot resolution, case-insensitivity) + a live sysctl smoke test.
  • HIDSendDeadlineTests — race mechanics, cancellation propagation, and the invalidateOnly classification pin.
  • HIDRebootRecoveryTests (opt-in E2E, registered in scripts/test-runner.sh) — tap → simctl shutdown && boot → tap through the same daemon (pidfile-asserted), with a dtuhidd-confounder guard.

Verification

  • make build / make test: 1115 unit tests green, no warnings.
  • SIMULATOR_UDID=… ./scripts/test-runner.sh HIDRebootRecoveryTests on Xcode 27 Beta 4 / iOS 27.0: passed live (27 s).
  • Manual protocol on an iOS 26.5 runtime with the fixed binary: baseline tap → reboot → tap through the surviving daemon (same pid 21858 throughout) — landed.

🤖 Generated with Claude Code

onevcat added 2 commits July 23, 2026 11:47
The boot-identity gate keyed cache reuse solely on the mtime of
<dataDirectory>/var/run/launchd_bootstrap.plist. The issue #55
investigation showed CoreSimulator does not reliably rewrite that
marker on every boot (observed frozen across reboots during the
Xcode 27 B4 / CoreSimulator 1169.1 session of 2026-07-22), so two
different boots could carry equal mtimes and the daemon kept sending
HID events into the previous boot's dead mach port — reporting
success while delivering nothing.

The token is now compound: the launchd_sim process identity
(pid + kernel start time — that process's lifetime IS the boot, and
the start time guards pid reuse) is authoritative, with the marker
mtime kept only as a fallback when the process probe is unavailable.
The probe reads the process table via sysctl(KERN_PROC_ALL) +
KERN_PROCARGS2 (~1-2 ms; the /bin/ps pattern used elsewhere measures
~40 ms, too slow to run per verb).

As defence in depth, each HID send now runs under a deadline
(default 30 s, SIM_USE_HID_SEND_TIMEOUT_MS overrides, 0 disables):
a send into a dead port that previously hung the daemon forever now
fails loudly. The timeout message deliberately matches none of
HIDPerformRecovery's dead-transport markers so a timed-out composite
is never blind-retried (pinned by a unit test).

New opt-in E2E suite HIDRebootRecoveryTests covers the regression
end-to-end: tap -> simctl shutdown && boot -> tap through the same
daemon; a pidfile assertion guards against a daemon respawn making
the run vacuous.

Verified live on Xcode 27 Beta 4 (iOS 27.0) and an iOS 26.5 runtime:
tap lands after an out-of-band reboot through a surviving daemon,
same daemon pid throughout.

Fixes #55

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

Signed-off-by: onevcat <onevcat@gmail.com>
Two review findings on #58:

- HIDSendDeadline trapped on the ms→ns multiply for any parseable
  SIM_USE_HID_SEND_TIMEOUT_MS above ~UInt64.max/1e6. Saturate instead:
  an absurdly large value now means "effectively no deadline"
  (UInt64.max ns ≈ 584 years), pinned by a regression test.

- HIDRebootRecoveryTests could leave the shared simulator shut down
  when the mid-test boot/bootstatus failed, cascading unrelated
  failures through the rest of the sequential runner. Best-effort
  re-boot before surfacing the real error.

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

Signed-off-by: onevcat <onevcat@gmail.com>
@onevcat
onevcat merged commit 6b8d8c2 into main Jul 23, 2026
4 checks passed
@onevcat
onevcat deleted the fix/hid-boot-identity-process-token branch July 23, 2026 03:15
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.

iOS daemon holds a stale HID connection across simulator reboot — tap reports success but is not delivered

1 participant