Q3 iter-2 — event-driven dispatch clears HTTP/2, REPLY_CHANNEL closes on dispatch-order bug¶
Status: verified — completed 2026-04-18
iter-2 replaced iter-1's upfront replay with an event-driven
dispatcher that emits server frames in reaction to matching
client frames on the same stream. Verdict: HTTP/2 framing layer
accepted (no more PROTOCOL_ERROR GOAWAY), full XPC handshake
exchanged on both channels, but CDS closed REPLY_CHANNEL with
RST_STREAM(stream=3, error_code=5 STREAM_CLOSED) because our
dispatch table fires the big Handshake (replay #8) on
client DATA(s1) instead of on client DATA(s3). Full writeup:
findings.md.
iter-3 (dispatch table v2: relocate #8/#9 onto client DATA(s3)
trigger, split client DATA(s1) into a counter) is the next
implementation pass.
What iter-2 does¶
- Listener
iosmux-spike-listener.pycommits74e8d61replace the upfront replay loop with adispatch_replay()method invoked fromhandle_frame()for each client frame. - Dispatch table keyed on
(frame_type, stream_id, flags)with trigger-oncedispatched_*flags to avoid double-emit. - Deploy to havoc
/tmp/iosmux-spike-listener.py, start withIOSMUX_SPIKE_REPLAY=1, trigger CDS viakillall CoreDeviceService+devicectl list devices. - Pull session artifacts, decode, compare with iter-0 pcap dispatch reference.
Files¶
findings.md— full decode of the iter-2 session, per-frame table, RST_STREAM analysis, iter-3 plan.results/session-01.log— per-frame listener log.results/session-01-recv.bin— 217 bytes received from CDS (preface + 9 frames ending in RST_STREAM).results/session-01-send.bin— 14326 bytes sent to CDS (same corpus as iter-1, different order).results/iosmux-listener.log,results/tunneld-spike.log— lifecycle logs, redacted.
What changed vs. iter-1¶
| Aspect | iter-1 | iter-2 |
|---|---|---|
| Replay strategy | Upfront, all 10 frames before reading | Event-driven, in reaction to client frames |
| CDS rejection type | HTTP/2 PROTOCOL_ERROR GOAWAY | RemoteXPC STREAM_CLOSED on stream 3 |
| CDS stopped at | After its first DATA(s1) | After full handshake on both s1 and s3 |
| Recv bytes | 180 (preface + 5 frames) | 217 (preface + 9 frames) |
| Debug text | "request HEADERS: invalid stream_id" |
None (RST_STREAM has no diagnostic) |
| Failure layer | HTTP/2 stream ownership | RemoteXPC channel ordering |
Dispatch rules (iter-2, as deployed)¶
client SETTINGS(s0, non-ACK) → emit replay #0, #1, #2
client HEADERS(s1) → emit replay #3, #4, #5
client DATA(s1), first only → emit replay #8, #9 ← BUG (fires too early)
client HEADERS(s3) → emit replay #6
client DATA(s3) → emit replay #7
The bug is the client DATA(s1) line: #8 is the substantive 14 KB
Handshake response and should wait until CDS has finished opening
REPLY_CHANNEL (i.e. until after client DATA(s3)). See
findings.md for the iter-3
proposal.
Reuse of iter-1 corpus¶
The 10-frame IPHONE_REPLAY_FRAMES tuple in
../iter-01/iphone_replay_bytes.py
is unchanged. iter-2 re-imports the same module and emits exactly
the same 14326 bytes — only the dispatch timing changed. This keeps
the iter-0/iter-1 redaction provenance intact and lets iter-2 isolate
a pure ordering-level variable.