Q3 iter-4 findings — dropping replay #9 removes CDS's RST_STREAM, reveals CDS is silently waiting for more¶
Status: verified — 2026-04-19
Live run on havoc. iter-4 listener (commit 1cd564e) differs
from iter-3 by exactly one thing: the DATA(s3) trigger emits
[7, 8] instead of [7, 8, 9]. Send buffer shrank from 14326 B
to 14313 B (= −13 B, the one dropped RST_STREAM frame). Recv
shrank from 217 B to 204 B (= −13 B, CDS's RST_STREAM(s3) that
was present in iter-3 is gone). No GOAWAY, no RST_STREAM, no
client-side close signal of any kind. Every byte below decoded
through standard HTTP/2 framing.
Update 2026-04-19: root cause identified and fixed
D.6.0-B surfaced that the "quiescent state" below actually
ended with CDS closing TCP after ~30 s of silence.
D.6.1-B then confirmed hypothesis H1 (placeholder UUID
as session-progression gate): swapping the 16-byte zero
placeholder in #8 for a fresh RFC 4122 v4 UUID per session
made CDS hold the connection ESTABLISHED for the full 130 s
observation window. The iter-4 byte-level findings below
were always correct — the dispatch table, frame ordering,
and wire output are all valid. The handshake was blocked
solely by the 16-byte UUID field redacted at source in
iphone_replay_bytes.py. With that one field patched, iter-4
behaviour becomes "CDS accepts handshake and waits
indefinitely" (as originally described), not "CDS times out
after 30 s". See
iter-06-pair-trigger/findings.md
for the timeout observation and
iter-07-uuid-patched/findings.md
for the fix and confirmation.
TL;DR¶
Hypothesis 1 confirmed: our replay #9 RST_STREAM(s1,
STREAM_CLOSED) was the direct cause of iter-2/iter-3's
RST_STREAM(s3, STREAM_CLOSED) from CDS. CDS was reciprocating: it
saw our immediate server-side reset on s1 as "server gave up
mid-handshake" and matched the error on the remaining stream (s3).
New state: with #9 dropped, CDS no longer emits any teardown
signal. It sends its full 8-frame handshake up to DATA(s3)
INIT_HANDSHAKE (all consistent with iter-2/iter-3 client behaviour
for those frames), receives our full 9-frame reply (#0–#8),
then goes silent. No further client-to-server bytes. No GOAWAY.
No RST. The TCP connection stays open indefinitely from CDS's
side; iter-4 was ended by pkill on the listener.
Forward progress: qualitative. Recv byte count went down, but the signal it went down means CDS is no longer rejecting the exchange — it has simply reached the end of its canned handshake output and is waiting for server-initiated next-step work that our listener does not provide.
This is the expected boundary of a pure replay strategy. Further progress requires either: (a) simulating whatever server-side RemoteXPC service call CDS expects next, or (b) accepting that research phase C has answered what it can answer and moving to Phase D backend implementation.
The session, byte by byte¶
Setup¶
- Listener bound
[::1]:34719,IOSMUX_SPIKE_REPLAY=1, iter-4 dispatcher (commit1cd564e). - SPIKE tunneld (PID 1952) unchanged from iter-1 chain.
- CDS restart via
killall CoreDeviceService,devicectl list devicesto drive one tunnel query.
Client → listener: 8 HTTP/2 frames, 204 bytes total¶
| # | 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 |
No RST_STREAM(s3), no GOAWAY, no further frames. Byte-exact
with iter-3's recv for frames 0 through 7. Frame 8 (RST_STREAM)
that iter-3 had is simply absent — CDS had no reason to send it
because it was never provoked by our #9.
Listener → CDS: 9 HTTP/2 frames, 14313 bytes total¶
Dispatches:
| client trigger | server frames emitted | notes |
|---|---|---|
| SETTINGS(s0, non-ACK) | #0, #1, #2 | unchanged from iter-3 |
| HEADERS(s1) | #3 | unchanged |
| DATA(s1) #1 | #4 | unchanged |
| HEADERS(s3) | #6 | unchanged |
| DATA(s1) #2 | #5 | unchanged |
| DATA(s3) INIT_HANDSHAKE | #7, #8 | #9 dropped — this is the one change |
All 9 emitted dispatches landed on wire. Post-#8 the listener
stayed in its recv() loop; CDS sent nothing; the 30-second read
timeout would have fired eventually, but the agent-driven trigger
script killed the listener ~1 second after devicectl list devices
returned.
Why recv shrank instead of growing¶
iter-3's recv = 217 B included CDS's 13-byte RST_STREAM(s3,
STREAM_CLOSED) reciprocating our #9. iter-4 removed the
provocation, so CDS never emitted the reciprocation. The 13-byte
delta is exactly that one frame. Recv 217 − 13 = 204.
"Smaller recv" sounds like regression; it is not. It is the absence of an error signal. The frame CDS emitted in iter-3 was a diagnostic-of-our-mistake, not progress. Removing our mistake removed CDS's diagnostic.
What iter-4 confirmed¶
#9is load-bearing for iter-3's symptom. Drop it, the symptom vanishes.- CDS's RST_STREAM(s3) in iter-3 was reciprocal, not
independent. That kills the initially tempting alternate
hypothesis that CDS had some other problem with s3 (e.g., the
INIT_HANDSHAKEflag semantics or the0x00400001flag word). - CDS accepts our
#814 KB Handshake dict with zeroed identifiers without complaint — no reject, no GOAWAY, no error code. The identity-validation hypothesis (iter-3 #2) is still open but less load-bearing than it looked. - Replay strategy has a clear ceiling: it can reach a quiet end state, but cannot itself cause CDS to progress further.
What iter-4 did NOT resolve¶
- What CDS expects next. It is silent but attentive. Likely it
is waiting for the real-device-side to initiate a specific
lockdown-over-XPC service call (e.g.
com.apple.mobile.lockdownvia one of the 62 service ports advertised in our Handshake dict). Until we synthesize that, CDS will not send a pair request. - Whether zeroed identifiers matter at all. With CDS gone
silent, we cannot distinguish "accepted identity but waiting"
from "rejected identity silently and waiting for deadline".
iter-5 could test this by restoring plausible identifiers in
#8and seeing if the silence becomes faster or the same.
Implications for Stage S2 and Phase D¶
The iter-1 through iter-4 arc closes Q3 in the strongest empirical form available from a pure-replay approach:
- iter-1: basic HTTP/2 stream-ownership rules matter (upfront replay rejected with PROTOCOL_ERROR).
- iter-2: event-driven dispatch is necessary, but naïve trigger mapping causes semantic-layer aborts.
- iter-3: frame ordering is not the root cause of the application layer abort.
- iter-4: our own server-initiated reset was the direct cause of that abort, and removing it lets CDS reach a quiescent state.
Phase D implication: a Go-based backend that serves the spike-tunneld endpoint can clear the HTTP/2 + RemoteXPC handshake using the exact state-machine captured here. The next unknown — "what RemoteXPC service call comes after the handshake" — is a Phase D research target, not a Phase C blocker. Phase C's original question was "what protocol does CDS speak to tunneld?" Answered: plain h2c, RemoteXPC envelope, reachable quiescent state after handshake. Details of post-handshake service choreography are Phase D scope because they are tied to whatever flow we expose first (pair is a user-initiated action, so its own request-reply is the natural thing to observe on the backend).
Recommended close-out: stop at iter-4. Do not continue iter-5+ under replay. Move the investigation into Phase D backend, where we can respond to CDS's actual service requests instead of pre-recording them.
Artifacts (committed, redacted)¶
Under
results/
alongside this file. Same redaction policy as iter-02/iter-03:
text logs scrubbed for VM hostname, tunnel ULA, USB link-local
IPv6. Binary files untouched (iphone_replay_bytes.py corpus
already redacted at source).
results/session-01-recv.bin— 204 bytes.results/session-01-send.bin— 14313 bytes (= 14326 − 13).results/session-01.log— 4990 bytes. Ends mid-read (no=== session end ===line) because the listener waspkill'd before the 30-second read timeout.results/iosmux-listener.log— 138 bytes.results/tunneld-spike.log— redacted SPIKE tunneld output.
How to reproduce¶
On havoc (assumes iter-4 deployment still in place):
ssh havoc-root 'killall CoreDeviceService 2>/dev/null || true'
ssh havoc 'devicectl list devices 2>&1 | tail -15'
# leave for 10-30 seconds to observe CDS's silence
ssh havoc 'pkill -f iosmux-spike-listener.py'
Status of the iter-4 chain¶
- Step A (drop #9): delivered in commit
1cd564e. - Step B (deployment to havoc): delivered.
- Step C (trigger CDS + capture): delivered.
- Step D (decode + findings): this document.
Iter-4 closes, and closes Q3 under the replay approach. Any
further progress on the s2c direction goes through Phase D backend
work, not through more iphone_replay_bytes.py iterations.