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 offset0x3364, 10 bytes printable ASCII, fill byte'0'(0x30). Same length + printable-ASCII validation discipline as UDID.IOSMUX_HANDSHAKE_ECID@ fixture offset0x34EC, 8 bytes little-endian uint64. Spec accepts decimal ("1224229469044766") or prefixed hex ("0x0004596E22A0401E") viastrconv.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:
- Survey host-side state that CDS could be reading to set
pairingState: unpaired: /var/db/lockdown/<UDID>.plist(classic Apple lockdown pair record); existence?- CoreDevice's paired-devices database (exact path TBD; likely
under
/var/db/MobileDevice/or/Library/Application Support/com.apple.*); schema? - Any keychain entries keyed by UDID or ECID.
- On havoc, with the iPhone registered in CoreDevice, inspect what
pairingStatereads from when reported asunpaired. Compare against a hypothetically paired device (our iPhone may or may not already be paired at the lockdown level — verify separately). - 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:
EthernetMacAddressis 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]andscripts/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 devicesshows 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 fix2bcea98) - 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.