Skip to content

Q-D66 history — Q-D66-1, Q-D66-2, Q-D66-13, Q-D66-14 Resolution logs

Status: verified — historical archive

Resolution log entries for Q-D66-1 (lockdown handler reachability, RESOLVED), Q-D66-2 (Mercury Codable Output bytes, RESOLVED for PairAction), Q-D66-13 (lockdown port stability, RESOLVED), and Q-D66-14 (Xcode spinner gate, RESOLVED). Archived from d66-research-questions.md on 2026-05-02 anchor refactor. The main doc retains the questions table and links here for full per-question history.

Q-D66-13 part (a) — empirical confirmation that ports are per-session (2026-04-28)

iter-10's capture (held local at /home/op/backups/iosmux/pcaps/) recorded com.apple.mobile.lockdown.remote.trusted at port 50367. On session 13's apparatus (iPhone iOS 26.4.2, BuildVersion 23E261) pymobiledevice3 remote rsd-info reports the same service at port 54311 with all other Services-dict entries in the 54262–54311 range, not the iter-10 50309–50370 range. iter-11 already noted the range but session 11 promoted 50367 to a constant during sub-task 1 implementation — this resolution makes the per-session reality explicit.

The implication for the architecture is binding: any code path that needs to talk to iPhone's classic-lockdown surface must discover the port from the live Services dict at runtime; no captured value is a safe constant.

Evidence: notes/iosmux-d66-session13-state.md §"Empirical finding from deploy attempt 1" (compaction-survivable scratch in the working tree). Backend log line on havoc:

Error: source dict: fetch dict from [fd5e:d3c4:5a8e::1]:50367:
  lockdown bootstrap dial: dial tcp [fd5e:d3c4:5a8e::1]:50367:
  connect: connection refused

pgrep against current apparatus confirmed the actual listener was on port 54311.

Q-D66-13 part (b) — RESOLVED, 2026-04-28, commit b2da3f6

Native Go RSD-greeting client landed. New file internal/lockdown/rsd_discovery.go exports FetchServices and FindServicePort. The serve subcommand's loadOrBootstrapDict in cmd/iosmux-backend/cmd/serve.go was reworked to a 3-step bootstrap:

  1. Resolve tunnel-address + tunnel-port via tunneld HTTP API (lockdown.ResolveTunnelAddress).
  2. Open an h2c RemoteXPC greeting client to [tunnel-address]:tunnel-port, receive the iPhone's unilateral 14 KB big-Handshake DATA frame on stream 1, decode the XpcWrapper payload via internal/xpc/, return the 62-entry Services dict (lockdown.FetchServices).
  3. Look up com.apple.mobile.lockdown.remote.trusted in the Services dict; use its discovered port to dial the classic lockdown service and run the iter-11 3-verb dialog (lockdown.FindServicePort + existing lockdown.FetchDict).

The hardcoded iPhoneLockdownPort=50367 constant is removed. No fallback values remain in any production code path; a missing or malformed Services entry surfaces a wrapped error and the backend exits, per ADR-0006.

Empirical verification — multi-cycle live test

Two consecutive iPhone tunneld sessions on havoc, each with a fresh ./scripts/iosmux-restore.sh between runs, cross-checked against pymobiledevice3 remote rsd-info ground truth:

Round tunnel-address tunnel-port pymd3 ground truth backend discovered Match
1 fd5e:d3c4:5a8e::1 52949 54311 54311
2 fd78:fddc:73f3::1 52953 54421 54421

Each round also reported services_total=62 and harvested the 88-key GetValue dict from the discovered port within ~7 s including the ~5 s tunneld discovery latency. Backend logs at /Users/nullweft/iosmux/iosmux-backend.log on havoc preserved (rotates on next restart).

Tests in internal/lockdown/rsd_discovery_test.go cover FetchServices against a fake-iPhone fixture player, the helper's happy / missing / invalid-port paths, dial failure, and peer GOAWAY. All pass under go test -race ./... plus the darwin cross-compile.

Why the iter-01 fixture's port (55493) is unrelated

iter-11/findings.md tables that reference 50367 describe the iter-10 pcap (/home/op/backups/iosmux/pcaps/iosmux-d10-real.pcap, gitignored), captured during a separate research session. The fixture file internal/backend/fixtures/iter01_big_handshake_14124.bin (bytes that our backend currently emits as the greeting big Handshake) carries lockdown.remote.trusted: Port=55493 baked in, which is what the rsd_discovery_test.go fixture-player asserts. Both 50367 and 55493 are fossilised per-session captures; neither is a stable runtime constant. Production code never hardcodes either.

Q-D66-1 — RESOLVED (2026-04-28): NO, sub-task 1 alone insufficient

Wide-pcap experiment on havoc, immediately after sub-task 1 deploy finished and tunneld was swapped to SPIKE mode advertising tunnel-port:34719 at our backend. Trigger: xcrun devicectl device info details --device E8A190DD-….

Capture method: tcpdump -i lo0 "tcp and not port 49151" running on havoc-root for the full duration of the trigger. Pcap held at ~/backups/iosmux/pcaps/iosmux-d13-q1.pcap (host-side, gitignored, 18 KB / 79 packets).

Result: exactly one TCP SYN observed — ::1:50316 → ::1:34719 (CDS opening the SPIKE-advertised greeting listener). Zero SYNs to any service port: no :50367 (synthetic listener), no :55493 (fixture-advertised lockdown.remote.trusted), no :54311/:54421/:5xxxx of any shape. The exchange after the SYN is the standard h2c greeting + RemoteXPC frames terminated by CDS without any back-connect attempt.

This empirically confirms iter-12's prediction with sub-task 1 deployed: CDS bails upstream of consulting the Services dict regardless of (a) trigger choice (device info details matches iter-10's lockdown-flow trigger), (b) whether a sub-task 1 listener is up at any port, © port-mismatch concerns between synthetic-listener bind (:50367) and fixture-advertised port (:55493). The trust gate fires before Services is read.

devicectl surface output identical to iter-12 (post-Q-D66-13(b) deploy):

Failed to load provisioning paramter list ...
pairingState: unpaired
tunnelState: unavailable
Error: An error occurred while communicating with a remote
process. (com.apple.dt.CoreDeviceError error 3 / Mercury 1000)

Implication for the plan: sub-task 3 (Mercury Codable interceptor for AcquireDeviceUsageAssertion and the static-success action set per ADR-0009 §Consequences) is now empirically the critical path. Sub-task 1 deployment was necessary infrastructure (closes Q-D66-13 and gives us a reachable lockdown surface) but the live CDS decision tree never reaches it. Phase 2 (Mercury logging interpose) gives us the byte-shapes for Q-D66-2; Phase 3 (GAMBIT) wires the interceptor into the inject so CDS gets a synthetic success reply and _shadowUseAssertion.fulfilled flips on Xcode-side per pair-button-and-cfnetwork.md §"Pair button gating".

Q-D66-2 — partial progress, 2026-04-28 (session 14 Round 3): Pair INPUT envelope captured

After commits dffe662 (recv-side interpose) and b252961 (two-tier recv filter relaxing the strict mangledTypeName check on anonymous peers), a redeploy + Pair-button-click round on havoc captured exactly one loose-recv event at the moment of the click — /tmp/iosmux-mercury-raw/000017-recv.{bin,txt} on havoc, redactions log shows coredevice_uuid fired correctly, 664 bytes structural dump.

The captured envelope is NOT the Mercury Codable {mangledTypeName, value} shape. It is a flat 6-key dict keyed by Apple-reverse-DNS action discriminator:

{
  "CoreDevice.actionIdentifier":             "com.apple.coredevice.action.pair",
  "CoreDevice.invocationIdentifier":         <UUID v4 per call>,
  "CoreDevice.deviceIdentifier":             <target CoreDevice UUID, 36-char string>,
  "CoreDevice.coreDeviceVersion":            { components, stringValue },
  "CoreDevice.CoreDeviceDDIProtocolVersion": <int64>,
  "CoreDevice.input":                        { endpoint: <xpc_endpoint_t> }
}

Full envelope analysis with implications for sub-task 3 design lives in mercury-envelope-empirical.md §"Pair action invocation envelope (session 14 Round 3, 2026-04-28)"; the falsification of the older Mercury-Codable-only model is reflected there in the new TL;DR ("Mercury XPC carries TWO distinct envelope shapes") and in action-interception-full-picture.md top-of-file !!! warning admonition pointing at the same section.

Status remains partial:

  • Pair INPUT envelope: ✅ captured byte-exact (this round).
  • Pair OUTPUT (success-reply) envelope: ❌ still pending. CDS crashes in Codable type-metadata init through CDS+0xB89B during dispatch, before forming the reply. 5 separate crash reports on 2026-04-28 confirm the same swift_retain / MetadataCacheEntryBase::doInitialization signature. Capture options: (a) Phase 4 reserve — friend's working CDS+device pair produces a real success reply; (b) sub-task 3 intercepts the request before dispatch and synthesises a reply from a reference shape derived from any successfully-replying action on the same apparatus.
  • Other action IDs (AcquireDeviceUsageAssertion, Connect, Disconnect, plus the rest of ADR-0009 §Consequences static-success set): ❌ pending capture from each action's matching UI trigger. Pair button alone only emitted com.apple.coredevice.action.pair; AcquireDeviceUsageAssertion would need install / debug-attach / app-launch to fire.

Q-D66-2 — partial progress, 2026-04-28 (session 14): recv-side coverage extension landed

Session 14 verification run drove the full Pair-button flow on havoc under the session-13 send-only capture rig (commit dc4d004). 20 events captured, all dir="send" CDS→Xcode broadcasts; the priority targets AcquireDeviceUsageAssertionActionDeclaration and PairActionDeclaration were absent. Root cause: send-only interposes catch CDS-as-sender and miss Xcode→CDS requests (which reach CDS through the connection event-handler block, not via send calls back from CDS).

Commit dffe662 adds a fourth DYLD_INTERPOSE entry on xpc_connection_set_event_handler that wraps the user-supplied event-handler block, filters by the same top-level mangledTypeName rule, dumps via the shared iosmux_mercury_dump_event() helper with dir="recv", then forwards to the original block. Capture happens before CDS dispatches the message into Codable invoke, so the byte-shape is preserved even when downstream dispatch crashes (the session-14 Pair-button flow also produced an EXC_BAD_ACCESS in swift_retain whose stack returns through CDS+0xB89B, the byte after the S1.B passthrough trampoline at CDS+0xB896 on invoke(anyOf:usingContentsOf:); the crash is a separate downstream issue and not in scope for the capture rig).

Status remains partial until the recv-side build is deployed on havoc and a Pair-button-flow re-run captures the AcquireDeviceUsageAssertion.Input byte shape (and ideally the inner mangled type names visible in the CoreDevice.ServiceEvent recv side too). Once those are committed as fixtures under internal/backend/fixtures/mercury/, the question moves to fully resolved.

Background detail in docs/research/coredevice-internals/mercury-envelope-empirical.md §"Coverage limitation discovered 2026-04-28 (session 14)".

Q-D66-2 — RESOLVED for PairAction, 2026-04-28 (session 14 iter 1-17)

PairActionDeclaration.Output = CoreDevice.DeviceConnectionChangeResult Codable schema fully empirically derived through 17 deploy-and-click iterations on havoc with a live iPhone. Each iteration peeled exactly one schema element (key name / type form) by reading the precise Apple decoder error out of the unified log, then committing the fix. After iter 17 Apple's decoder accepts the entire reply:

Xcode: (CoreDevice) [com.apple.dt.coredevice:pairing]
    Pairing attempt completed with error nil

Full byte-level schema (every field, every type, every wire form, including the xpc_uuid vs xpc_string quirk, the Int64 vs UInt64 distinction, the auto-Codable vs String-rawValue enum split, and the DeviceIdentifier custom-Codable named keys) is in gambit-pair-action-schema.md. That document is the authoritative spec — reference it when extending GAMBIT to handle other allow-listed actions.

Commits in the iter 1-17 trail: b808eae (initial GAMBIT) … 49c8f1e (monotonicIdentifier as Int64). Live trace through 8 fixes preserved in git log for compaction-survivable reconstruction.

The OTHER actions in the 15-action allow-list (AcquireDeviceUsageAssertion, ListUsageAssertions, EnableDDIServices, DisableDDIServicesAction, FetchDDIMetadata, UpdateHostDDIs, RemoveHostDDIs, GetTrainName, DarwinNotificationObserve/Post, Tags) remain open. They may share DeviceConnectionChangeResult (Connect/Disconnect/Unpair are semantically alike; the same Output type is plausible) or have different Output types. When their UI triggers fire, decoder errors will name the Output type and the same iter-and-peel methodology applies.

A new question Q-D66-14 was added above for the spinner-state UI transition that PairAction's Output success does not by itself trigger.

Q-D66-14 — RESOLVED, 2026-04-29 (session 16): spinner gate is kvoCache_isPaired Bool ivar, gated by RDSUE broadcast filtered on monotonicIdentifier

Three-probe research chain across session 16 closed Q-D66-14 to a clean root cause and shipped fix:

  1. iosmux-uigate-probe.md — disasm of DVTCoreDeviceCore identified the spinner gate as DVTCoreDevice_Impl.kvoCache_isPaired : Swift.Bool (private ivar field-offset entry 0x6e9c0). Read by isPaired.getter (one-byte ivar read, no remote query) and consumed by upstream isIgnored.getter / DVTDeviceOperation._connectAndPrepareImplicitly which produces the "skipping implicit preparation for device being ignored with reason: deviceIsUnpaired" log line. Setter chain: init(with:) registers @Sendable (UUID, DeviceInfo) -> () callback on a CoreDevice stream, callback runs _updatePropertiesImpactingAvailability which reads DeviceInfo.pairingState.getter and writes kvoCache_isPaired. NO Mercury Codable type literals are visible inside DVTCoreDeviceCore — the dispatcher lives upstream in CoreDevice.framework.

  2. iosmux-cdstream-probe.md — disasm of CoreDevice.framework traced the (UUID, DeviceInfo) stream end-to-end. The addDeviceInfoChanged(on:handler:) API is on CoreDevice.RemoteDevice (NOT DeviceManager), backing storage _stateStorage. RDSUE chain: EventManager.receiveEvent → typed handler for ServiceEventKind.remoteDeviceStateUpdateDeviceManager.handle(deviceUpdateSnapshot:)RemoteDevice.updateState(from:markServiceConnected:)_stateStorage setter → addDeviceInfoChanged callback fires. Critical filter: DeviceStateSnapshot.monotonicIdentifier: Int64. RemoteDevice.updateState evidently silently drops snapshots whose monotonicIdentifier is ≤ the previously cached value (no decoder error, no log, just a stale-event filter). Apple's CDS-native flow incremented this counter to 3+ before our PairAction click landed.

  3. Fix landed: commit 648f531 seeds GAMBIT's monotonic counter from mach_absolute_time() at install time (nanoseconds-since-boot, ~10^11 right after boot). Per-emit __atomic_add_fetch(+1) keeps strict monotonicity. Apple's counter incrementing from 0 by ones cannot reach this baseline within a session, so our broadcasts are guaranteed strictly newer than any prior cache.

  4. Empirical confirmation (session 16, post-deploy Pair click): spinner cleared. Xcode device row transitioned out of "Xcode has already started pairing…" state for the first time. Apple unified log no longer shows skipping implicit preparation ... deviceIsUnpaired after our PairAction reply lands.

_shadowUseAssertion.fulfilled is the SEPARATE hasConnection / "Connected" UI category gate (Q-D66-15 below) — independent of kvoCache_isPaired. Two independent ivars on the same DVTCoreDevice_Impl class, 456 bytes apart in layout. Confirmed by the uigate probe.

Phase D.6.6-impl Phase 1 (commits 58ca63628d7d89) implemented per-action Output dispatch for all 15 allow-listed actions plus AFM endpoint side-channel for AcquireDeviceUsageAssertion — see Q-D66-15 for the AFM-acceptance follow-up.

See also