Skip to content

Phase D.6.6-research-b iter-15 findings — combined UDID + Serial + ECID patch FALSIFIES H-HS multi-field; close-timing reverts from iter-14's 3 s to iter-12's 30 s baseline

Status: verified — 2026-04-24

Backend handshakeBytes now supports 4 concurrent env-gated patches (UUID + UDID + SerialNumber + UniqueChipID/ECID). Fetched real identity values from the iPhone via pymobiledevice3 remote rsd-info --tunnel <UDID> during a brief stock-tunneld window. Patched Handshake at emit time with all three Properties identity fields matching tunneld's advertisement, ran the iter-12 devicectl device info details --device <CoreDevice-UUID> trigger, observed for 30 s with a wide lo0 pcap filter. Two independent post-fix runs (r3, r4): Handshake parses cleanly on CDS side (no RST_STREAM), devicectl returns the same pairingState: unpaired + tunnelState: unavailable + Mercury-class warning as iter-12 and iter-14, zero SYN to any advertised service port in the 30 s window. CDS closes the session at T+~30 s from Handshake — same timing as iter-12's all-zeros Handshake, markedly different from iter-14's ~3 s close with UDID-only patch. H-HS multi-field is falsified; H-HS is dead at both single-field (iter-14) and multi-field (iter-15) levels.

TL;DR

iter-14 falsified H-HS slot #1 (UDID alone) and flagged multi-field as the next test. iter-15 delivered it. The combined patch produces no forward progress and reverts CDS to the same 30 s timeout behaviour seen with zeroed identity fields in iter-12. The real delta between iter-14 and iter-15 is the direction of failure:

  • iter-12 (zero UDID): CDS holds the session 30 s, then local close. Interpretation: "I have no idea what device this is; wait and see."
  • iter-14 (UDID alone patched): CDS closes ~3 s after Handshake. Interpretation: "I recognize UDID; something else disagrees; reject fast."
  • iter-15 (UDID+Serial+ECID patched): CDS holds the session 30 s, then local close. Interpretation: "I recognize the device as a whole; but the host-side state I need for this device (pair record? tunnel session?) is missing; wait for it; timeout."

Reading across the three, the gate is not a wire-level identity check. If it were, CDS would have unblocked at iter-15 where the entire Properties identity trio matches tunneld's advertisement. Instead CDS reverts to "I don't know what to do" when the Handshake looks fully legitimate — strong evidence the trust gate is host-side state (CoreDevice paired-devices DB / lockdown escrow), not a Handshake field at all.

Bug caught by our own verbose decoder

iter-15 run #1 surfaced an off-by-2 in handshakeECIDOffset that all four ECID unit tests missed:

session=N verbose send DATA(s1, 14124 B):
    decode error: xpc: decode payload:
    xpc: read dict value for "Properties":
    xpc: read dict value for "UniqueChipID":
    xpc: unknown type code 0x401e4000 at offset 13512

The iter-14 zero-run scan for the UniqueChipID slot matched a 10-byte NUL run at 0x34EA..0x34F3, and I (erroneously) took the first byte of the run as the uint64 value offset. The XPC uint64 type tag is [00 40 00 00] LE = TypeUInt64 (0x00004000) and sits at 0x34E8..0x34EB, so the tag's three low bytes overlap the start of the apparent NUL run. The actual 8-byte uint64 body starts at 0x34EC, two bytes later.

Patching at 0x34EA corrupted the low bytes of the type tag from [00 40 00 00] to [00 40 1E 40] = 0x401E4000 — unparseable. Our own verbose decoder flagged it before the frame went on the wire; CDS on the peer side agreed and sent RST_STREAM on streams 1 and 3 immediately after our 14124-byte frame. Fix landed in commit 2bcea98 before the valid runs.

Testing lesson: unit tests verified "8 LE bytes at offset X decode as the expected uint64" but did not round-trip decode the full patched fixture as a plist. A future "iter-01 fixture round-trip decodes cleanly with all 4 patches applied" test would have caught this.

Method

Code change

Extended iter-14's backend scaffolding with two more slots:

  • IOSMUX_HANDSHAKE_SERIAL @ fixture offset 0x3364, 10 bytes printable ASCII, fill byte '0' (0x30). Same length + printable-ASCII validation discipline as UDID.
  • IOSMUX_HANDSHAKE_ECID @ fixture offset 0x34EC, 8 bytes little-endian uint64. Spec accepts decimal ("1224229469044766") or prefixed hex ("0x0004596E22A0401E") via strconv.ParseUint(..., 0, 64).

handshakeBytes signature now returns 5 values: (payload, patchedUUIDHex, patchedUDID, patchedSerial, patchedECIDHex). handshakeFixtureCheck DRY'd into a checkFixtureRegion helper that validates all 4 patch slots independently against their expected fill bytes. Patch independence preserved: a failing patch on one slot does not block the others.

7 new unit tests + 4 existing updated, all green with -race. Binary size: 5,826,896 B (+9 KB vs iter-14 binary).

Identity acquisition

The apparatus today lives in SPIKE mode most of the time, so the iPhone's real identity values are not readable via devicectl (which fails with "tunnelState: unavailable"). Workaround: briefly swap tunneld back to stock mode and run pymobiledevice3 remote rsd-info --tunnel <UDID> which returns the full Properties dict from the real iPhone's Handshake. Four identity fields extracted: UDID, SerialNumber, UniqueChipID, EthernetMacAddress (MAC not used in iter-15 — reserved for a later slot if iter-15 + 16 both fail).

Small cross-check: Apple iOS 17+ UDIDs follow the shape <chip-marker>-<ECID-hex-16>. Our device's UDID ends in 0004596E22A0401E and ECID decimal = 1224229469044766 = 0x0004596E22A0401E. The two values agree, confirming the identity data is consistent (and any multi-field cross-check CDS does should see a coherent picture).

Apparatus prep

The run sequence that surfaces-and-breaks CoreDevice state repeatedly:

1. VM reboot (clean slate)
2. sudo scripts/iosmux-restore.sh        (host-side USB, stock tunneld)
3. xcrun devicectl list devices          (verify iPhone registered
                                          with CoreDevice UUID
                                          E8A190DD-64F5-44A4-8D57-
                                          28E99E316D60)
4. scripts/iosmux-tunneld-mode.sh spike  (hoisted iter-10 swap script)
5. (run iter-15 experiment within ~5 min
    before CoreDevice state decays)

CoreDevice's device registry is fragile: after the first SPIKE-mode experiment per reboot, subsequent xcrun devicectl invocations tend to return "No devices found" and recovery paths (launchctl kickstart -k com.apple.remoted, stock-mode re-settle) do not reliably re-populate the registry. Known limitation documented in scripts/iosmux-coredevice-prime.sh; mitigation is "reboot + first run counts" plus reproducibility collected in the same run window.

Experiment parameters

  • Backend env: IOSMUX_BACKEND_VERBOSE=1, IOSMUX_HANDSHAKE_UUID=random, IOSMUX_HANDSHAKE_UDID=<tunneld-advertised-UDID>, IOSMUX_HANDSHAKE_SERIAL=<real-10-char>, IOSMUX_HANDSHAKE_ECID=<real-decimal-uint64>.
  • Trigger: xcrun devicectl device info details --device <CoreDevice-UUID> --timeout 10.
  • Observation window: 30 s sleep after trigger, then teardown.
  • Pcap: tcpdump -i lo0 -U -n -w … 'tcp and not port 49151' (same wide filter as iter-09 / iter-12 / iter-14).

Results

Run-by-run summary

Run Patches applied Handshake parsed by CDS Close timing SYN to any advertised port Notes
r1 UDID + Serial + buggy-ECID NO (RST_STREAM on streams 1 + 3) — (RST at T+~0.1 s) 0 to advertised; 1 to 127.0.0.1:62078 Buggy ECID offset corrupted the XPC type tag. The :62078 SYN is a CDS fallback to classic usbmuxd after the RSD path rejected — an artifact, not a back-connect signal. Discarded for hypothesis scoring; retained as the diagnostic that caught the offset bug.
r2 (not run) Between r1 and r3 I swapped tunneld and re-deployed; CoreDevice dropped registration briefly and devicectl returned "device not found". Apparatus recovery cost one run cycle.
r3 UDID + Serial + ECID (fixed) YES (clean plist) T+30 s, backend local close 0 Primary data point.
r4 UDID + Serial + ECID (fixed) YES (clean plist) T+31 s, backend local close 0 Reproducibility.
r5 (not fully run) CoreDevice decayed after r4; session=2 did not fire, only session=1 preface-EOF. Apparatus tells us the reboot-warm window expired.

The primary data is r3 and r4 — both runs of the fixed multi-field patch with CoreDevice in a healthy registered state. Both show the same timing (~30 s) and the same SYN topology (zero back-connects).

devicectl output (r3 and r4, identical to iter-12 and iter-14)

Gathering device information...
WARNING: Unable to retrieve complete information for this device.
         Error: An error occurred while communicating with a remote process.
Current device information:
? identifier: <CoreDevice-UUID>
? hardwareProperties: iPhone SE (3rd generation), iPhone14,6
? deviceProperties: ddiServicesAvailable: false
? connectionProperties:
    ? pairingState: unpaired
    ? tunnelState: unavailable

Same Mercury-class warning as iter-12 / iter-14. pairingState: unpaired is invariant across all three configurations of the Handshake. This is the single strongest piece of evidence that the gate is host-side: wire-state changes (zero identity / one identity / three identities) cannot move the reported pair state off unpaired.

Backend verbose log (r3, the primary data point)

session=2 verbose send   UUID (patched)   => 25699f2e-ecb0-49dd-9644-cadfd8d15815
session=2 verbose send   UDID (patched)   => 00008110-XXXXXXXXXXXXXXXX
session=2 verbose send   Serial (patched) => XXXXXXXXXX
session=2 verbose send   ECID (patched)   => 0xXXXXXXXXXXXXXXXX
session=2 verbose send DATA(s1, 14124 B): flags=0x101 msgid=2
session=2 verbose send   MessageType => string "Handshake"
session=2 verbose send   MessagingProtocolVersion => uint64 7
session=2 verbose send   Services => <dict 62 entries>
session=2 verbose send   Properties => <dict 46 entries>
session=2 verbose send   UUID => <uuid 25699f2eecb049dd9644cadfd8d15815>
session=2 dispatcher: emit #8 DATA(s1, 14124 B big Handshake)
  [... 30 s of silence ...]
session=2 read loop: local close

All four patches land, our own verbose decoder walks the full Properties dict (46 entries) without error, the fixture goes on the wire, CDS acknowledges (no RST_STREAM), then nothing for 30 s, then local close on our side.

Pcap — all 5 SYNs (r3)

T+0.000  ::1.49232 > ::1.34719  Flags [S]           (probe, before backend ready)
T+0.568  ::1.49235 > ::1.34719  Flags [S]           (session=1 preface-EOF)
T+0.568  ::1.34719 > ::1.49235  Flags [S.]
T+2.074  ::1.49237 > ::1.34719  Flags [S]           (session=2 full Handshake)
T+2.074  ::1.34719 > ::1.49237  Flags [S.]

Unique client-side destination ports in SYNs: :34719 only. No probe to :50367 (the advertised .remote.trusted lockdown port), no probe to :50333 (.remote.untrusted), no probe to any other advertised service port, no fallback to 127.0.0.1:62078 (the classic-usbmuxd port that did appear in r1 after the Handshake RST). The 30-s observation window after Handshake is entirely quiescent except for the orderly session close.

Close-timing comparison

iter Handshake Properties identity fields Close timing Plausible interpretation
iter-12 All zeros (no patches) ~30 s (local close) CDS has no info to decide; waits for something; times out.
iter-14 UDID alone patched ~3 s (peer close) UDID cached somewhere on host; second check (Serial? ECID?) fails; definitive reject.
iter-15 UDID + Serial + ECID patched ~30 s (local close) Identity looks coherent; host-side pair-state lookup is the gate; lookup finds no pair record / waits for establishment; times out.

The iter-15 reversion to the iter-12 baseline is the load-bearing finding. If the gate were "all identity fields cross-check tunneld-advertised values", iter-15 should have either (a) unblocked (SYN to :50367) or (b) closed fast like iter-14 did. Neither happened. The only way to explain a fully-coherent identity → 30 s timeout is that CDS consults a host-side store that our Handshake content cannot influence.

Hypothesis disposition

  • H-HS single-field slot #1 (UDID): FALSIFIED by iter-14.
  • H-HS multi-field (UDID + Serial + ECID): FALSIFIED by iter-15. H-HS is dead at both sensible formulations.
  • H-Adv (tunneld advertisement fields other than UDID/Serial): still untested; iter-15 does not bear on it.
  • H-PairState (host-side CoreDevice DB / lockdown escrow): promoted to primary working hypothesis by iter-15. The iter-12 ↔ iter-14 ↔ iter-15 timing progression fits a model where the gate reads host-side state and our wire-state patches merely shift how that lookup is triggered, not what it returns.
  • H-Services (full Services sub-dict byte diff): cheap side-channel left untested; still worth a fallback iter if H-PairState turns out to be wrong.
  • H-Tx (transport-level signal): still parked as low-likelihood.

Proposed next step

Pivot to H-PairState investigation. Specifically the read-only pass first:

  1. Survey host-side state that CDS could be reading to set pairingState: unpaired:
  2. /var/db/lockdown/<UDID>.plist (classic Apple lockdown pair record); existence?
  3. CoreDevice's paired-devices database (exact path TBD; likely under /var/db/MobileDevice/ or /Library/Application Support/com.apple.*); schema?
  4. Any keychain entries keyed by UDID or ECID.
  5. On havoc, with the iPhone registered in CoreDevice, inspect what pairingState reads from when reported as unpaired. Compare against a hypothetically paired device (our iPhone may or may not already be paired at the lockdown level — verify separately).
  6. Decide whether "plant a fake pair record" is feasible without corrupting live iPhone usage. If the real iPhone is already paired at the classic USB layer, the existing record could be a useful reference for structure. Backup discipline mandatory (user-memory rule: backup before any write in ~/backups/<project>/).

This is a substantive iter-16 scope — we are no longer looking at Handshake wire bytes, we are looking at macOS identity/trust DB schemas. Likely takes a full session's context budget to complete the read-only pass alone. The write-side pair-record plant is a separate follow-up iter.

If H-PairState also proves inert, the remaining angles are H-Adv and H-Services, both mechanical to test with the existing scaffolding.

What iter-15 did not test

  • MAC address patch: EthernetMacAddress is the remaining identity field in Properties we have not patched. Cheap to add (another offset, another env gate, 3 unit tests) but iter-15's multi-field result already refutes "some identity subset unlocks the gate" — adding MAC is unlikely to change the picture.
  • BootSessionUUID patch: same reasoning; skipped unless a specific signal motivates adding it.
  • The Services sub-dict contents: iter-13 assumed identical by construction. Confirming that with a byte-diff is still a cheap fallback test if H-PairState also fails.

Apparatus notes

  • scripts/iosmux-tunneld-mode.sh [spike|stock] and scripts/iosmux-coredevice-prime.sh (committed in iter-15 scaffolding, 9ed728c) are the two reusable tools produced this iter. Both smoke-tested end-to-end in this session.
  • CoreDevice cold-prime after a fresh reboot is implicit in the restore script path — do NOT swap tunneld modes until after a verified xcrun devicectl list devices shows the iPhone.
  • After the first SPIKE-mode experiment, plan for CoreDevice state decay. Reproducibility runs collected in the same 5-minute window.

Status

  • Step A (implement Serial + ECID patching): delivered (scaffolding 9ed728c, ECID offset fix 2bcea98)
  • Step B (run H-HS multi-field experiment with valid plist): delivered, 2 clean data points (r3, r4), consistent results.
  • Step C (interpret + propose next): delivered, H-HS FALSIFIED at multi-field, H-PairState promoted.

iter-15 closes the H-HS branch of D.6.6-research-b. The next research frontier is host-side pair state.

Reproduce

# Clean slate: reboot VM, then from Linux host:
sudo scripts/iosmux-restore.sh
ssh havoc 'xcrun devicectl list devices'    # MUST show iPhone row
scripts/iosmux-tunneld-mode.sh spike
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build \
    -o /tmp/iosmux-backend-darwin ./cmd/iosmux-backend/
scp /tmp/iosmux-backend-darwin havoc:/tmp/iosmux-backend-d15
# (deploy the iter-15 d15-*.sh scripts as documented in the scaffolding
# commit; runtime identity values via
# `pymobiledevice3 remote rsd-info --tunnel <UDID>` during a brief
# stock window if needed.)

# Single run:
ssh havoc-root "bash /tmp/iosmux-d15-start-pcap.sh"
ssh havoc "bash /tmp/iosmux-d15-start-backend.sh <UDID> <SERIAL> <ECID>"
ssh havoc "bash /tmp/iosmux-d15-trigger.sh <CoreDevice-UUID>"
ssh havoc "bash /tmp/iosmux-d15-teardown.sh"
ssh havoc-root "bash /tmp/iosmux-d15-teardown-root.sh"

# Expected: session=2 full Handshake, 30 s silence, local close.
# Unexpected (would be the H-PairState answer): SYN to [::1]:50367
# in the post-Handshake window.