Q3 iter-3 findings — strict pcap order reproduced, STREAM_CLOSED(s3) unchanged — ordering ruled out as root cause¶
Status: verified — 2026-04-19
Live run on havoc. Real CoreDeviceService connected to the iter-3
spike listener (commit c842b56) running in
IOSMUX_SPIKE_REPLAY=1 mode with the dispatch table v2 that
matches the Q2 pcap's s2c emit order exactly. All 10 replay
frames dispatched (send = 14326 B, full corpus). CDS still
RST_STREAM'd stream 3 with STREAM_CLOSED. Same outcome as
iter-2 at the HTTP/2 layer — which means the iter-2 post-mortem's
"dispatch ordering" hypothesis is disproven. Every byte below
decoded through standard HTTP/2 framing and pymobiledevice3's
XpcWrapper.parse.
TL;DR¶
iter-3 reorganised the dispatcher so its trigger→emit map produces the same wire order the real iPhone used in the Q2 pcap:
pcap iPhone s2c order: #0 #1 #2 #3 #4 #5 #6 #7 #8 #9
iter-3 listener s2c order: #0 #1 #2 #3 #4 #6 #5 #7 #8 #9 ← #5/#6 swapped
^^^^^
The #5/#6 swap relative to the pcap is an artifact of CDS's c2s
order, not our dispatcher: CDS sends HEADERS(s3) before its
second DATA(s1) (the sync 0x0201), so our event-driven scheme
fires #6 before #5. In the pcap the iPhone had the opposite
luxury (it chose when to emit its own frames unilaterally). The
semantic content is identical — #5 and #6 are on different
streams and a reader does not care about their inter-stream order.
Outcome: recv still 217 bytes (same as iter-2), client frame
sequence identical to iter-2, CDS still ends with
RST_STREAM(stream=3, error_code=5 STREAM_CLOSED) followed by TCP
RST. Forward progress this iteration = 0. But the diagnostic
value is high: we now know the problem is NOT frame ordering.
Remaining hypotheses (to be tested by iter-4+):
- Our emission of
#9 RST_STREAM(s1, STREAM_CLOSED)immediately after#8(14 KB Handshake) may confuse CDS's state machine even though the real iPhone does the exact same thing in the pcap. Possibly CDS expects some client→server acknowledgement of#8first, and sees the immediate RST_STREAM as "server aborted before acknowledgement" → closes REPLY_CHANNEL. - The redacted identifiers inside
#8'sHandshakedict (UniqueDeviceID = 25 ASCII zeros, BootSessionUUID = 16 binary zeros, top-level UUID = 16 binary zeros, EthernetMacAddress = 17 ASCII zeros, SerialNumber = 10 ASCII zeros, UniqueChipID = 8 zero bytes) do not match the UDID that tunneld advertised to CDS. CDS may be validating identity consistency between its RSD tunnel advertisement and the device-self-identification in the Handshake. - Timing: iter-3 emits all 10 frames within ~10 ms. The real iPhone in the Q2 pcap had natural per-frame spacing (USB round trips between kernel and device). CDS may have per-frame deadlines.
iter-4 will test hypothesis 1 first (cheapest — drop #9, see if
CDS proceeds on s1 past the Handshake). If that does not move the
needle, iter-5 will test hypothesis 2 (a synthesised Handshake
dict with the tunneld-advertised UDID in place of the zeroed one).
The session, byte by byte¶
Setup¶
- Listener bound
[::1]:34719,IOSMUX_SPIKE_REPLAY=1, iter-3 dispatcher (commitc842b56). - SPIKE tunneld (same PID 1952 from iter-1 chain) still running
and still advertising
tunnel-address=::1, tunnel-port=34719. - CDS restart via
killall CoreDeviceService,devicectl list devicesto drive one tunnel query.
Client → listener: 9 HTTP/2 frames, 217 bytes total¶
Byte-exact with iter-2's recv. No change in client behaviour:
| # | type | stream | flags | payload len | wire len |
|---|---|---|---|---|---|
| — | (preface) | — | — | 0 | 24 |
| 0 | SETTINGS | 0 | 0x00 | 12 | 21 |
| 1 | WINDOW_UPDATE | 0 | 0x00 | 4 | 13 |
| 2 | HEADERS | 1 | 0x04 | 0 | 9 |
| 3 | DATA | 1 | 0x00 | 44 | 53 |
| 4 | SETTINGS | 0 | 0x01 | 0 | 9 |
| 5 | HEADERS | 3 | 0x04 | 0 | 9 |
| 6 | DATA | 1 | 0x00 | 24 | 33 |
| 7 | DATA | 3 | 0x00 | 24 | 33 |
| 8 | RST_STREAM | 3 | 0x00 | 4 | 13 |
All DATA payloads byte-exact with iter-2's recv (also byte-exact with Q1 and the Q2 pcap c2s side). No new bytes from CDS this iteration.
Listener → CDS: 10 HTTP/2 frames, 14326 bytes total¶
All 10 dispatches fired. Dispatch trace from
results/session-01.log:
| client trigger | server frames emitted | wall-clock |
|---|---|---|
| SETTINGS(s0, non-ACK) | #0 SETTINGS(12), #1 WINDOW_UPDATE(4), #2 SETTINGS-ACK | T+3 ms |
| HEADERS(s1) | #3 HEADERS(s1) | T+6 ms |
| DATA(s1) #1 (empty-dict) | #4 DATA(s1, 44 empty-dict mirror) | T+7 ms |
| HEADERS(s3) | #6 HEADERS(s3) | T+8 ms |
| DATA(s1) #2 (sync 0x0201) | #5 DATA(s1, 24 sync mirror) | T+9 ms |
| DATA(s3) INIT_HANDSHAKE | #7 INIT_HANDSHAKE mirror, #8 14 KB Handshake, #9 RST_STREAM(s1,STREAM_CLOSED) | T+11 ms |
Every dispatched_* flag and the data_s1_count counter fired
exactly as designed. No skipped frames, no repeated frames.
The RST_STREAM(s3) decoded¶
off=0xd0 type=RST_STREAM(0x03) stream=3 flags=0x00 len=4
payload (4 bytes):
00 00 00 05 error_code = 0x00000005 (STREAM_CLOSED)
Identical to iter-2's RST_STREAM — same stream, same error code,
same absence of any GOAWAY. Latency: CDS sent it ~3 ms after our
final #9. That is CDS processing all three final frames (#7,
#8, #9) before reacting.
What iter-3 ruled out¶
- Frame ordering is not the root cause. iter-3 matched the
pcap-observed iPhone emit order for all frames that have
cross-stream independence. Only
#5and#6are swapped, and they live on different streams (s1 and s3 respectively), so no reader on either stream can observe the swap. - Trigger dispatch logic is correct. Every mapping fired
cleanly,
data_s1_countreached 2 and both mirror frames landed,dispatched_data_s3fired#7/#8/#9as the composite response. - Frame framing is correct. CDS's h2c parser did not raise a single PROTOCOL_ERROR or FRAME_SIZE_ERROR — the failure is strictly at the application semantics layer.
What iter-3 did NOT change¶
- Server-bytes-on-wire: still 14326 bytes
- Client-bytes-on-wire: still 217 bytes
- CDS's teardown signal: still RST_STREAM(s3, STREAM_CLOSED)
- CDS's progression: stopped at exactly the same milestone
Implications for iter-4¶
The three remaining hypotheses are cheap to test sequentially:
Hypothesis 1: drop #9 RST_STREAM(s1)¶
Minimal change. Skip #9 from the DATA(s3) trigger's emit list:
Leaves ROOT_CHANNEL (s1) open after the big Handshake. If CDS
proceeds to emit anything new on s1 (or anywhere) after
seeing this, the #9 ≠ iPhone behaviour fringe is confirmed: even
though the pcap shows iPhone emitting #9, CDS sees our emulation
as "server gave up prematurely" and reciprocates on s3.
Hypothesis 2: unzero the identifiers in #8¶
Requires a source modification to iter-01/iphone_replay_bytes.py
(or a runtime overlay) to replace the six zeroed fields with the
tunneld-advertised UDID (and synthesised correlates for the
non-UDID fields). This is a protocol-legitimacy test: CDS may be
cross-checking device identity between its RSD advertisement and
the Handshake body.
Privacy constraint: the real device UDID never appears in any committed artifact. The synthesised replacement would use a generated GUID matching the redacted-field length, so the file stays public.
Hypothesis 3: introduce per-frame delay¶
Add time.sleep(0.002) between send_replay_indices calls.
Cheapest to try but least likely to matter — HTTP/2 parsers are
packet-boundary-insensitive.
Recommended iter-4: hypothesis 1 (drop #9). One-line change,
highest signal-to-noise.
Artifacts (committed, redacted)¶
Under
results/
alongside this file. Redaction policy identical to iter-02:
.log files scrubbed for VM hostname, tunnel ULA, USB link-local
IPv6. Binary files left as-is (send-side is the already-redacted
iphone_replay_bytes.py corpus; recv-side is CDS's handshake
envelope with no UDID content).
results/session-01-recv.bin— 217 bytes, CDS's full transmission (preface + 9 frames ending in RST_STREAM(s3)). Byte-exact with iter-2's recv.results/session-01-send.bin— 14326 bytes, all 10 replay frames rendered onto the wire.results/session-01.log— 5492 bytes, per-frame listener log.results/iosmux-listener.log— 138 bytes, listener lifecycle.results/tunneld-spike.log— redacted SPIKE tunneld output.
How to reproduce¶
On havoc (assumes the iter-3 deployment is still up):
ssh havoc-root 'killall CoreDeviceService 2>/dev/null || true'
ssh havoc 'devicectl list devices 2>&1 | tail -15'
Pull artifacts with scp. Decode session-01-recv.bin with any
HTTP/2 framer + XpcWrapper parser (magic 0x29B00B92).
Status of the iter-3 chain¶
- Step A (dispatcher v2): delivered in commit
c842b56. - Step B (deployment to havoc): delivered.
- Step C (trigger CDS + capture): delivered.
- Step D (decode + findings): this document.
Iter-3 closes. The ordering hypothesis is empirically
disproven. Iter-4 (drop #9) is the next implementation pass.