Phase D.6.1-B findings — hypothesis H1 (placeholder UUID) CONFIRMED STRONGLY¶
Status: verified — 2026-04-19
Breakthrough. D.6.0-B observed CDS tearing down the TCP
connection at 23-36 s after our #8 big Handshake with the
16-byte zero placeholder UUID. D.6.1-B swapped that UUID for
a fresh RFC 4122 v4 UUID per session (via the D.6.1-A
IOSMUX_HANDSHAKE_UUID=random env var, commit ffe7c29)
and CDS held the connection for the full 130-second
observation window with no EOF, only TCP keepalives every
~15 s. H1 is CONFIRMED: the zero UUID was the session-
progression gate, and patching it unblocks CDS's handshake
acceptance.
TL;DR¶
The iter-6 30-second timeout is gone. D.6.1-B session 1 held TCP ESTABLISHED for 130 s (orchestrator's kill window), with CDS sending TCP keep-alive ACKs every ~15 s as the only client→server traffic. Backend PID 5839's FIN was the only session terminator — CDS did not disconnect on its own.
But: CDS did not send any new application-layer traffic
either. After our #8 big Handshake, CDS goes truly silent at
the HTTP/2 layer. It neither disconnects (good, H1 gate passed)
nor initiates a service request (not-so-good, we still don't
know what it expects next).
This is a new third state beyond iter-4 "killed before timeout" and iter-6 "killed by CDS":
| iter | UUID in #8 |
session fate |
|---|---|---|
| iter-1 → iter-3 | zero (redacted) | CDS disconnects via PROTOCOL_ERROR or RST_STREAM |
| iter-4 → D.5 | zero (redacted) | CDS TCP ESTABLISHED, pkill within 13 s, timeout unobserved |
| D.6.0-B | zero (redacted) | CDS closes TCP with peer EOF at 23-36 s (30 s window observed 3x) |
| D.6.1-B | random v4 per session | CDS holds TCP ESTABLISHED, 130 s observed, no EOF, no new traffic |
The sessions¶
Setup¶
- Backend:
cmd/iosmux-backend/, commitffe7c29, cross-compileddarwin/amd64(5,808,976 B Mach-O x86_64 PIE). - Env:
IOSMUX_HANDSHAKE_UUID=random IOSMUX_BACKEND_VERBOSE=1. - SPIKE tunneld: PID 5731 (freshly re-armed after the iter-6
tunnel died); advertising
tunnel-port:34719for the iPhone's UDID.
Session timeline¶
Single CDS connection observed:
| event | timestamp | elapsed since handshake |
|---|---|---|
| Backend start | 02:49:50 | — |
| accept session=1 | 02:50:05.382 | t₀ |
| Full iter-4 dispatch emitted (#0-#8) | 02:50:05.382 | +0 ms |
| UUID patched for this session | 02:50:05.382 | 8529522e-248a-4641-a72a-8557fc17aa6d (v4) |
| TCP keep-alive observed | 02:50:20 | +15 s |
| TCP keep-alive observed | 02:50:35 | +30 s |
| TCP keep-alive observed | 02:50:50 | +45 s |
| ... (keep-alives continue every 15 s) | ... | ... |
| TCP keep-alive observed | 02:52:06 | +121 s |
| Backend kill (pkill) | 02:52:16 | +131 s |
| Local FIN sent | 02:52:16 | +131 s |
D.6.0-B's 3 sessions died at +36 s, +32 s, +23 s. D.6.1-B's
session 1 hit +131 s with zero CDS-side disconnect. The pcap
confirms TCP state was ESTABLISHED on both ends at the
moment we sent our FIN.
Per-session UUID¶
One UUID per session because D.6.1-B captured only one session.
In future D.6.x iterations each new CDS connection will get a
fresh v4 — the patching happens at every #8 emit, not once
per backend lifetime.
Version nibble 4 at position 12 (4641) and variant bits 10xx
at position 16 (a72a) confirm proper RFC 4122 v4 layout.
What this reinterprets (second-order clarifications)¶
iter-04/findings.md and iter-05-go-backend/findings.md were updated after D.6.0-B with a clarification callout: "CDS silently evaluates the handshake, decides it is unusable, disconnects." That clarification is true for the zeroed fixture but not inherent to our handshake structure. With a session-bound UUID, the same dispatcher produces an accepted handshake and a long-lived connection.
The original iter-4 dispatch-table work and the D.5 Go backend are therefore not semantically broken — they were blocked by a single 16-byte field redacted at source. The byte-level framing, stream topology, XPC envelopes, and service dict structure were all correct; only the UUID identity fingerprint was missing.
D.5's "byte-exact replaces the Python listener" claim still holds: Python and Go both emit the same 14,313 B server-side stream. The difference between iter-4 (Python 30 s timeout) and D.6.1-B (Go, 130 s no timeout) is purely the UUID patching, which the Python listener does not do.
What D.6.1-B did NOT resolve¶
CDS accepts the handshake, but doesn't initiate anything next. The original D.6.0 goal — observing CDS's first post-handshake RemoteXPC service request — is still unmet. The only post-handshake wire traffic during 130 s was TCP keep-alives (stack-level, not HTTP/2).
Candidate explanations for the new silence (ranked by likelihood, NOT verified):
H1a: CDS expects server-initiated traffic (not client-initiated)¶
In real iPhone flows, the iPhone proactively pushes heartbeat or
service-advertisement updates. Our dispatcher emits nothing past
#8 ever. CDS may be genuinely idle, waiting for us to say
something.
Test: extend the dispatcher to emit a PING on stream 0 every
N seconds after #8, observe whether that triggers CDS to
respond.
H1b: CDS waits for a specific user-side action¶
devicectl device info still returned error 1000 "device not
found" even with accepted handshake. The error path in
CDS may be triggered by missing Services that our Handshake
advertises but that are not reachable (ports CDS tries to
connect to but no one listens on).
Test: instrument the dispatcher to log any new TCP connection
attempt to the ports in our advertised Services dict. If CDS
tries com.apple.lockdown.remote.trusted:55451, we'd see a
second loopback connection.
H1c: CDS's state machine is deterministic and simply has nothing to say¶
Some services may require devicectl pair as a different
starting action. Pair is the only action that initiates
service traffic from CDS rather than reading a cached state.
Test: try devicectl manage pair with the UUID-patched backend
running.
Recommended D.6.2: test H1c first (cheapest — no code changes, just a different trigger). If pair trigger produces new traffic, we have the first actual post-handshake request to decode. If pair also gives silence, try H1b (passive port- listen instrumentation). H1a requires dispatcher changes, save for last.
Artifacts¶
Under
iter-07-uuid-patched/:
verbose-session.log— 3,144 B, 40 lines. Single session with the patched UUID line clearly visible before#8.iosmux-d7.pcap— 19,806 B loopback capture. Scanned for real iPhone UDID (upper, lower, binary) — zero hits. Safe to commit as-is.
No redaction needed on the log — the backend-generated v4 UUID is ephemeral session state we invented, not PII. Pcap contains only our handshake bytes + TCP keep-alive frames.
How to reproduce¶
On havoc (assumes apparatus armed with SPIKE tunneld):
# Cross-compile + deploy with UUID patching ON
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
ssh havoc 'chmod +x /tmp/iosmux-backend'
ssh havoc 'nohup env IOSMUX_HANDSHAKE_UUID=random IOSMUX_BACKEND_VERBOSE=1 \
/tmp/iosmux-backend -listen "[::1]:34719" \
> /tmp/iosmux-backend.log 2>&1 &'
# Trigger + WAIT past the iter-6 30 s timeout
ssh havoc-root 'killall CoreDeviceService'
ssh havoc "devicectl device info details --device <UDID>"
sleep 90
# Observe: log should NOT contain 'peer closed (EOF)'
ssh havoc 'grep -c "peer closed (EOF)" /tmp/iosmux-backend.log'
# Expected: 0
Status of the D.6.1 chain¶
- Step A (UUID patching code): delivered in commit
ffe7c29. - Step B (deploy + re-arm + observe): delivered, H1 confirmed strongly. This document.
- Step C (next hypothesis test): D.6.2 with H1c (pair trigger) first.
D.6.1 closes. The handshake-rejection mystery is resolved: it was the zero UUID. D.6.2 picks up the "what does CDS expect us to send next" question with the handshake-gate unblocked.