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:
- Resolve tunnel-address + tunnel-port via tunneld HTTP API
(
lockdown.ResolveTunnelAddress). - 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 viainternal/xpc/, return the 62-entry Services dict (lockdown.FetchServices). - Look up
com.apple.mobile.lockdown.remote.trustedin the Services dict; use its discovered port to dial the classic lockdown service and run the iter-11 3-verb dialog (lockdown.FindServicePort+ existinglockdown.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
INPUTenvelope: ✅ 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 sameswift_retain/MetadataCacheEntryBase::doInitializationsignature. 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 emittedcom.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:
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:
-
iosmux-uigate-probe.md— disasm ofDVTCoreDeviceCoreidentified the spinner gate asDVTCoreDevice_Impl.kvoCache_isPaired : Swift.Bool(private ivar field-offset entry0x6e9c0). Read byisPaired.getter(one-byte ivar read, no remote query) and consumed by upstreamisIgnored.getter/DVTDeviceOperation._connectAndPrepareImplicitlywhich 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_updatePropertiesImpactingAvailabilitywhich readsDeviceInfo.pairingState.getterand writeskvoCache_isPaired. NO Mercury Codable type literals are visible inside DVTCoreDeviceCore — the dispatcher lives upstream in CoreDevice.framework. -
iosmux-cdstream-probe.md— disasm ofCoreDevice.frameworktraced the(UUID, DeviceInfo)stream end-to-end. TheaddDeviceInfoChanged(on:handler:)API is onCoreDevice.RemoteDevice(NOTDeviceManager), backing storage_stateStorage. RDSUE chain:EventManager.receiveEvent→ typed handler forServiceEventKind.remoteDeviceStateUpdate→DeviceManager.handle(deviceUpdateSnapshot:)→RemoteDevice.updateState(from:markServiceConnected:)→_stateStoragesetter →addDeviceInfoChangedcallback fires. Critical filter:DeviceStateSnapshot.monotonicIdentifier: Int64.RemoteDevice.updateStateevidently silently drops snapshots whosemonotonicIdentifieris ≤ 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. -
Fix landed: commit
648f531seeds GAMBIT's monotonic counter frommach_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. -
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 ... deviceIsUnpairedafter 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 58ca636 … 28d7d89) 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¶
d66-research-questions.md— main table; current open questions including Q-D66-15.d66-history-q15-d21-d33.md— Q-D66-15 D21..D33 Resolution logs.d66-history-q15-d34-d40.md— Q-D66-15 D34..D40 Resolution logs.