Skip to content

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+):

  1. 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 #8 first, and sees the immediate RST_STREAM as "server aborted before acknowledgement" → closes REPLY_CHANNEL.
  2. The redacted identifiers inside #8's Handshake dict (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.
  3. 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 (commit c842b56).
  • 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 devices to 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

  1. 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 #5 and #6 are swapped, and they live on different streams (s1 and s3 respectively), so no reader on either stream can observe the swap.
  2. Trigger dispatch logic is correct. Every mapping fired cleanly, data_s1_count reached 2 and both mirror frames landed, dispatched_data_s3 fired #7/#8/#9 as the composite response.
  3. 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:

client DATA(s3)  → #7, #8      (was: #7, #8, #9)

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.