Skip to content

Phase D.6.6-research-b iter-14 findings — UDID patch alone does not unblock CDS back-connect to :50367 (H-HS narrowed form, slot #1, FALSIFIED)

Status: verified — 2026-04-24

Added env-gated UDID patching to the Go backend Handshake emitter (IOSMUX_HANDSHAKE_UDID=<25-char-UDID> at fixture offset 0x34BC, mirroring D.6.1's UUID patch mechanism at 0x371C). Deployed to havoc, ran the iter-12 wide-pcap trigger (devicectl device info details) with the UDID matching tunneld's advertisement. Result across four independent runs: Handshake carries the patched UDID verbatim (backend verbose log confirms), devicectl reports pairingState: unpaired + tunnelState: unavailable + Mercury-class warning (identical to iter-12), and zero SYN packets to any advertised service port appear in the 30-second post-trigger observation window. The only TCP SYNs in the wide pcap are the client's own probes and real connections to our [::1]:34719 listener (2–3 per run). The H-HS narrowed form slot #1 (UDID) is falsified at the UDID-alone-sufficient level.

TL;DR

iter-13 ranked UniqueDeviceID as the #1 candidate trust-gate field by a wide margin. iter-14 tests that prediction directly by patching the fixture at emit time with the real tunneld- advertised UDID. The patch lands (verified in backend verbose log every run) but CDS still does not back-connect to any advertised service port. H-HS narrowed form with UDID-alone is refuted.

A secondary empirical signal is worth flagging: CDS closes the session 3 seconds after receiving the patched Handshake, versus ~30 seconds with iter-12's zero-UDID Handshake. The faster disposition is evidence the UDID is being consumed and evaluated — just not in a way that progresses us past the trust gate. Interpretation: there is at least one additional check (likely host-side pair-record lookup in CoreDevice's paired-devices DB) that our wire-state patching cannot reach.

Method

Four runs against the same iPhone, same tunneld session (PID 2337 throughout), with the backend re-launched per run via a scripted nohup pattern.

Code change

Extended internal/backend/handshake.go with the same shape as the D.6.1 UUID patch:

  • handshakeUDIDEnv = "IOSMUX_HANDSHAKE_UDID"
  • handshakeUDIDOffset = 0x34BC (value start of the fixture's UniqueDeviceID entry; derived by byte-scanning the fixture for the adjacent '0'-run next to the "UniqueDeviceID" ASCII key — see /tmp/iosmux-d14-inspect-strings.py)
  • handshakeUDIDLen = 25
  • handshakeUDIDFill = 0x30 ('0' ASCII; surprising finding — iter-13's decoder reported "all-zero ASCII", which we initially read as NUL-filled, but the fixture actually holds 25 × 0x30 printable-'0' bytes. Scan confirmed only three NUL-byte zero-runs of length ≥ 8 in frame #8: the ECID slot @ 0x34EA, the BootSessionUUID slot @ 0x3594, and the top-level UUID slot @ 0x371C. UDID/Serial/MAC all use '0' padding, keeping the string values valid UTF-8.)
  • Validation: exactly 25 bytes, every byte in the printable range [0x20, 0x7E]; on violation, log a session-tagged warning and leave the region untouched (same failure discipline as the UUID patch).
  • Independence: the UUID and UDID patches are applied in a single pass over a freshly copied buffer; a failing patch on one slot does not block the other. A unit test pins this (TestHandshakePatchIndependence).

Full tests: TestHandshakePatchUDIDFixed, TestHandshakePatchUDIDInvalidLength, TestHandshakePatchUDIDInvalidBytes, TestHandshakePatchUUIDAndUDIDBothApply, TestHandshakePatchIndependence — all green with -race.

Apparatus

  • iPhone SE (3rd gen), iOS 26.4.x, USB-tethered to havoc
  • Tunneld (PID 2337) serving HTTP API on 127.0.0.1:49151, advertising tunnel-port: 34719 at ::1
  • Hardware UDID (what tunneld returns + what we patch into Handshake): 00008110-XXXXXXXXXXXXXXXX (25-char iOS 17+ dashed form; real value present in held-local runtime logs, never committed)
  • CoreDevice identifier for the device (what devicectl wants as --device arg, distinct from the hardware UDID — see Discovery below): <CoreDevice-UUID> (36-char canonical UUID)
  • SPIKE backend built from the new code: GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build → binary at havoc:/tmp/iosmux-backend-d14, 5,817,904 B. Deployed via scp.

Trigger + observation

  • Backend env: IOSMUX_BACKEND_VERBOSE=1, IOSMUX_HANDSHAKE_UUID=random (kept on from D.6.1), IOSMUX_HANDSHAKE_UDID=<tunneld-UDID> (the iter-14 new patch).
  • Pcap: tcpdump -i lo0 -U -n -w … 'tcp and not port 49151' (same wide filter as iter-09 / iter-12).
  • Trigger: xcrun devicectl device info details --device <CoreDevice-UUID> --timeout 10, then sleep 30 to give CDS a wide window for async back-connects. 30 s was chosen to exceed iter-12's observed ~30 s local-close timeout.
  • Teardown: SIGTERM backend, SIGINT tcpdump, scp artifacts back to Linux host for analysis.

Scripts (held local, not committed — they contain live UDID values in shell history):

/tmp/iosmux-d14-find-offsets.py       (fixture zero-run scan)
/tmp/iosmux-d14-inspect-strings.py    (fixture bytes per key)
/tmp/iosmux-d14-start-pcap.sh         (havoc-root)
/tmp/iosmux-d14-start-backend.sh      (havoc)
/tmp/iosmux-d14-trigger.sh            (havoc)
/tmp/iosmux-d14-teardown.sh           (havoc)
/tmp/iosmux-d14-teardown-root.sh      (havoc-root)

Discovery: tunneld UDID ≠ devicectl CoreDevice UUID

The first run accidentally passed the tunneld-advertised UDID as devicectl --device arg, expecting them to be the same identifier. devicectl rejected it with CoreDeviceError Code=1000 "The specified device was not found". Running xcrun devicectl list devices clarified:

Name              Hostname   Identifier                             State                Model
---------------   --------   ------------------------------------   ------------------   ----
iPhone (iosmux)              <CoreDevice-UUID>                      connected (no DDI)   iPhone SE (3rd gen)

The Identifier column is a 36-char canonical UUID generated per-host by CoreDevice when it first registers the device; it is NOT the hardware UDID. The hardware UDID (what tunneld keys by and what lives in the Handshake Properties UniqueDeviceID field) is the 25-char iOS 17+ form 00008110-XXXXXXXXXXXXXXXX.

iter-12's findings used the placeholder <devicectl-uuid> without spelling out which of these two identifiers it referred to; in practice it meant the CoreDevice UUID.

For future iterations this distinction matters: the hardware UDID goes into the wire Handshake; the CoreDevice UUID goes into devicectl --device. They are cross-referenced by CoreDevice internally.

Results

Run-by-run summary

Run devicectl arg devicectl rc session=1 session=2 SYN to :50367 / any advertised port Close timing
1 tunneld UDID (wrong) 1 (device not found) preface-EOF (probe) full HS 0 local close at T+12 s
2 CoreDevice UUID 0 (Mercury warning) preface-EOF (probe) 0 no session=2 in the window
3 CoreDevice UUID 0 (Mercury warning) preface-EOF (probe) full HS 0 peer closed (EOF) at T+3 s
4 (wide window) CoreDevice UUID 0 (Mercury warning) preface-EOF (probe) full HS 0 peer closed (EOF) at T+3 s, no further traffic in the following 27 s

Run #1 is a methodology artefact (wrong identifier); the handshake completed but was driven by a different caller (some CoreDevice background daemon, not devicectl). The primary result table is runs #2–#4 with the correct trigger argument.

devicectl output (runs #2–#4, abridged)

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

Identical in kind to iter-12's output — pairingState: unpaired + tunnelState: unavailable + Mercury-class warning. The patched UDID does not move devicectl's reported pair-state off unpaired.

Backend verbose log (run #4, the full-handshake run)

Each Handshake emission in runs #1/#3/#4 logs the applied patches before the 14124-byte frame goes on the wire:

session=N verbose send   UUID (patched) => <fresh v4 UUID>
session=N verbose send   UDID (patched) => 00008110-XXXXXXXXXXXXXXXX
session=N verbose send DATA(s1, 14124 B): flags=0x101 msgid=2
session=N verbose send   MessageType => string "Handshake"
session=N verbose send   MessagingProtocolVersion => uint64 7
session=N verbose send   Services => <dict 62 entries>
session=N verbose send   Properties => <dict 46 entries>
session=N verbose send   UUID => <uuid …>

Both patches land every run. The fixture's adjacent bytes outside the patch regions are preserved (unit-tested; also checked by byte-diff against iter01BigHandshake14124 in a spot-check script during development).

Pcap: SYN enumeration (run #4, 30 s window)

T+0.000  ::1.52497 > ::1.34719  Flags [S]           (probe)
T+0.564  ::1.52500 > ::1.34719  Flags [S]           (session=1 = preface-EOF)
T+0.564  ::1.34719 > ::1.52500  Flags [S.]
T+1.531  ::1.52502 > ::1.34719  Flags [S]           (session=2 = full HS)
T+1.531  ::1.34719 > ::1.52502  Flags [S.]

Five TCP SYN / SYN-ACK records total. Two are client-initiated connections to our :34719 listener; the SYN-ACK pair are our replies. Client destination ports observed in SYNs: 34719 only.

Zero SYN to any of the 62 advertised services, including :50367 (.remote.trusted) and :50333 (.remote.untrusted). The 30 s post-trigger window is quiescent after T+1.531 except for the orderly close of session=2 at T+~3 s.

What changed vs iter-12 (same control, one var: UDID)

Aspect iter-12 (UDID = 25 × '0') iter-14 (UDID = real hardware UDID)
Code D.6.1 backend, UUID patching only + UDID patching
Handshake bytes reaching CDS 14124 B with '0'-padded UDID 14124 B with real UDID
devicectl reported state unpaired / unavailable / Mercury unpaired / unavailable / Mercury
SYN to :50367 0 0
Session close timing after Handshake ~30 s (CDS-side timeout) ~3 s (CDS-side disposition, much faster)
Close direction backend's local close peer closed (EOF)

The close-timing delta is the one real wire-level difference between the two treatments: patched Handshake produces a disposition roughly an order of magnitude faster. This is evidence our patched UDID is being read by CDS but interpreted as actionable for faster rejection, not as a gate passed.

What this means for H-HS

H-HS narrowed form, slot #1 (UDID), FALSIFIED at the UDID-alone-sufficient level. The patch lands, the bytes reach CDS, CDS appears to consume them (close speed-up), but the back-connect does not follow.

Three ways this failure can be accommodated without abandoning H-HS entirely:

  1. H-HS requires a multi-field match: CDS may cross-check UDID against SerialNumber and/or UniqueChipID (ECID), and any one of them being wrong (still '0'-padded in our fixture) causes the gate to fail. Testable by patching Serial / ECID alongside UDID — slots #2 and #3 of iter-13's shortlist.

  2. H-HS requires coherent values against tunneld advertisement metadata, not just a non-zero value: CDS may pull UDID from tunneld, pull UDID from Handshake, and check that the bytes agree exactly. We did match tunneld's advertised value — so this interpretation is partially falsified by iter-14. But other metadata (advertised interface fe80::d446:…%en0, tunnel-address ::1) also reach CDS via tunneld; a mismatch there could still be the proximate cause, unrelated to Handshake content.

  3. The gate is not in the Handshake at all — host-side state (CoreDevice's paired-devices DB, the lockdown pair-record escrow) is consulted and returns "unpaired", which short-circuits the back-connect regardless of Handshake content. This explanation is consistent with devicectl's very explicit pairingState: unpaired line.

Option 3 is the most austere explanation — it predicts that no amount of Handshake patching will unlock the back-connect, and that the real missing piece is either (a) actually pairing the device to the host, or (b) planting a fake pair-record in CoreDevice's DB to satisfy the local check.

Proposed next step

The cheapest follow-up that disambiguates between options 1 and 3 is a combined patch (UDID + Serial + ECID all patched to real tunneld-derived values) in a single backend session. If the combined patch still produces 0 SYNs to any advertised port, option 3 is strongly supported and research-b for H-HS should be declared dead. If the combined patch produces a SYN to :50367, H-HS multi-field becomes the working model and we iterate to the minimal satisfying set.

For the combined patch we need the real iPhone's SerialNumber and ECID. Both are available from:

  • xcrun devicectl list devices --json-output - (reports SerialNumber in the JSON tree)
  • /var/db/lockdown/ plists, if accessible (probably root required; may not be necessary)
  • ioreg -p IOUSB -l (ECID is exposed as USB Serial Number on the USB device node; SerialNumber likewise — this is what ioreg-based tools use)

The patch plumbing is straightforward — add IOSMUX_HANDSHAKE_SERIAL and IOSMUX_HANDSHAKE_ECID env gates at offsets 0x3364 (10 B ASCII) and 0x34EA (8 B LE uint64), with the same validation discipline as UDID and UUID.

If the combined patch fails: pivot to the pair-state angle. The question "can we plant a CoreDevice pair-record to satisfy the local check" is a separate non-trivial line of work; it may need its own iter-15+.

Anomalies worth flagging

  • session=2 completion is inconsistent across runs: runs 1, 3, and 4 saw session=2 complete a full handshake; run 2 saw only session=1 preface-EOF within the observation window. No clear deterministic cause. May be related to devicectl's internal retry / probe scheduler. Not the primary signal — the primary signal (zero back-connect SYNs) is invariant across all runs.
  • Faster close (3 s) with UDID patch: as noted above, this is a positive signal that UDID is consumed. Worth remembering for research-c and onward — any future patch that removes this speed-up and restores the 30-second timeout would be a regression and should be investigated.

Status

  • Step A (implement UDID patching): delivered (code + tests, all green with -race)
  • Step B (run H-HS slot-1 experiment): delivered (4 runs, consistent zero back-connect)
  • Step C (analyse + propose next): delivered (option 1 combined patch, option 3 pair-state angle)

iter-14 falsifies the UDID-alone form of H-HS. The next move is either the combined UDID + Serial + ECID patch (iter-15) or a pivot to the host-side pair-state angle.

Reproduce

# Build
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build \
    -o /tmp/iosmux-backend-darwin ./cmd/iosmux-backend/

# Deploy
scp /tmp/iosmux-backend-darwin havoc:/tmp/iosmux-backend-d14

# Apparatus check
ssh havoc "curl -sm 2 http://127.0.0.1:49151/"
# expect: {"<hardware-UDID>":[{"tunnel-address":"::1","tunnel-port":34719,...}]}

ssh havoc "xcrun devicectl list devices"
# expect: one row with Identifier=<CoreDevice-UUID>

# Run (copy/paste the five scripts under /tmp/iosmux-d14-*.sh in
# this iter's script list; run them in the order: start-pcap,
# start-backend <hardware-UDID>, trigger <CoreDevice-UUID>,
# teardown, teardown-root)

# Analyse
tcpdump -r /tmp/iosmux-d14.pcap -nn | grep -E 'Flags \[S'
# expect: only SYNs to :34719, zero to any other dst port