Stage S2.C self-experiment¶
Status: verified — runbook executed end-to-end
This runbook was actually run on 2026-04-13 against the patched
pymobiledevice3 v9.9.1 + the iosmux_wire_logger.m inject. Raw
artifacts in
results/.
Findings distilled in FINDINGS.md.
Artifacts for the one-shot Phase C self-experiment that resolves three of the Stage S2 blockers in a single run on the havoc VM:
- X (client-side) — what bytes does CoreDeviceService actually emit on its pair-flow RSD tunnel connection?
- Y — which PID inside the CDS process family calls
nw_connection_create_with_connected_socket(so we know whether ourLC_LOAD_DYLIBinjection reaches the right process)? - Z — does CDS's sandbox permit
AF_UNIXsockets,bind()to/tmp/*.sock, andsendmsg()withSCM_RIGHTSancillary data?
Full context is in ../../plan-stage2-pair-flow.md
and the prior-art capture in ../s2b-pair-attempt-log.md.
The research / devil's-advocate pass that produced these three
blockers is ../../plan-stage2-pair-flow.md
in its Phase C section.
Contents¶
iosmux-spike-listener.py— zero-deps Python 3 HTTP/2 capture listener. See its module docstring for the protocol it speaks. Intended to run on havoc and receive the redirected CDS connection enabled by../../patches/pymobiledevice3/0001-iosmux-spike-tunneld-endpoint-override.patch.- Future: a
README-findings.mdsummarising whatever we observe when the experiment runs. Added alongside the commit that lands the result.
One-shot runbook¶
All commands assume the iosmux checkout on the Linux host at
/home/op/dev/myrepos/iosmux and ssh access to the havoc VM as
havoc (user) and havoc-root (root).
- Deploy patched pymobiledevice3 (idempotent):
- Deploy the instrumented inject:
scp inject/*.m inject/Makefile havoc:/Users/nullweft/iosmux/inject/
ssh havoc 'cd /Users/nullweft/iosmux/inject && make iosmux_inject.dylib'
ssh havoc-root 'cp /Users/nullweft/iosmux/inject/iosmux_inject.dylib \
/Library/Developer/CoreDevice/iosmux_inject.dylib'
- Restart tunneld with
IOSMUX_SPIKE=1so it advertises[::1]:34719as the tunnel endpoint instead of the real iPhone:
ssh havoc-root 'pkill -f "pymobiledevice3 remote tunneld" || true'
ssh havoc-root 'IOSMUX_SPIKE=1 IOSMUX_SPIKE_PORT=34719 \
nohup /Users/nullweft/pymobiledevice3-venv/bin/pymobiledevice3 \
remote tunneld --host 127.0.0.1 --port 49151 \
> /tmp/iosmux-tunneld-spike.log 2>&1 &'
Verify:
ssh havoc 'curl -sm 2 http://127.0.0.1:49151/'
# expected: {"<udid>":[{"tunnel-address":"::1","tunnel-port":34719,...}]}
- Start the capture listener on havoc, bound to
[::1]:34719:
scp docs/research/protocol/s2c-self-experiment/iosmux-spike-listener.py \
havoc:/tmp/iosmux-spike-listener.py
ssh havoc 'python3 /tmp/iosmux-spike-listener.py --outdir /tmp/iosmux-capture'
Leave that running in a foreground terminal — it prints one line per accept() and writes per-session files.
- Trigger CDS to reach for the tunnel. Easiest path: restart
CoreDeviceService and run
devicectl list devices, or click Pair in Xcode's Devices window. Both make CDS consult tunneld, get back the spike-overridden endpoint, and try to connect to[::1]:34719.
ssh havoc-root 'killall CoreDeviceService 2>/dev/null || true'
ssh havoc 'devicectl list devices 2>&1 | tail -10'
- Collect results — stop the listener (Ctrl-C) and pull all three log sources:
mkdir -p docs/research/protocol/s2c-self-experiment/results
scp havoc:/tmp/iosmux_wire.log \
docs/research/protocol/s2c-self-experiment/results/
scp -r havoc:/tmp/iosmux-capture \
docs/research/protocol/s2c-self-experiment/results/
scp havoc:/tmp/iosmux-tunneld-spike.log \
docs/research/protocol/s2c-self-experiment/results/
iosmux_wire.log— sandbox probe errno + every interposednw_connection_*call with (pid, tid, args) — answers Y and Ziosmux-capture/session-NN-*— bytes CDS sent to the spike endpoint and what we sent back — answers the client half of X-
iosmux-tunneld-spike.log— tunneld's own stderr, useful if anything went sideways -
Revert to stock tunneld when done (so the normal device flow works again for other sessions):
ssh havoc-root 'pkill -f "pymobiledevice3 remote tunneld" || true'
./scripts/iosmux-restore.sh # restarts stock tunneld, no env var
What "success" looks like¶
/tmp/iosmux_wire.loghas a=== sandbox probe end ===block with errno=0 (or at least a clear EPERM) for each of the four syscall tests. Blocker Z closed./tmp/iosmux_wire.loghas at least onenw_connection_create_with_connected_socket fd=X params=Yline whosepid=Nfield matches a CDS instance we control. Blocker Y closed.iosmux-capture/session-01.loghas*** HTTP/2 preface matched ***followed by at least oneFRAME type=SETTINGSfrom the client. Blocker X (client half) closed. If instead it shows*** preface MISMATCH *** first byte=0x16then the earlier "no TLS" assumption is wrong and we pivot to investigating PSK.
What the experiment closed¶
Ran on 2026-04-13 against the patched pymobiledevice3 v9.9.1 + the
iosmux inject with iosmux_wire_logger.m added. Results land in
FINDINGS.md and the raw artifacts in
results/. Short version:
- Blocker X (client protocol) — plain prior-knowledge h2c,
no TLS. RemoteXPC DATA frames on streams ⅓ with XpcWrapper
magic bytes matching pymobiledevice3's
xpc_message.py. - Blocker Y (which PID) — same PID our inject loads into.
Current
LC_LOAD_DYLIBcoverage is sufficient. - Blocker Z (sandbox) — all four syscall probes passed.
AF_UNIX + socketpair +
/tmpbind + SCM_RIGHTS are all legal inside CoreDeviceService's sandbox.
Self-research queue (Q1-Q5) — all resolved¶
The experiment answered "what does CDS send". The Q1-Q5 follow-up answered "what does CDS expect back" — entirely on havoc, no external capture needed. Status as of 2026-04-18:
- Q1 (verified) — decoded the captured CDS bytes via
pymobiledevice3's own
XpcWrapper.parse. Classification: generic RemoteXPC connection init, byte-exact with pymobiledevice3's own_do_handshake. See Q1-decode.md. - Q2 (verified) —
tcpdumpon havoc'sutun4during a livepymobiledevice3 remote rsd-infoshows plaintext HTTP/2 prior-knowledge (h2c). No TLS. The pcap is the authoritative source for iPhone-side reply bytes. See Q2-utun4-capture.md. - Q3 iter-0 (verified) — reference dispatch table extracted
from the Q2 pcap. After h2c init the iPhone unilaterally sends a
single 14 KB
MessageType="Handshake"DATA frame on stream 1 (62 services + 46 properties), then RSTs the stream withSTREAM_CLOSED(error_code 5). Stream 3 stays open as the persistent channel. See Q3-iter-00-pcap-dispatch.md. - Q3 iter-1 (verified) — live replay of the iter-0 bytes at
CDS rejected with a clean
GOAWAY error_code=PROTOCOL_ERROR debug="request HEADERS: invalid stream_id". Root cause: HTTP/2 forbids the server from initiating streams on client-owned odd IDs. Fix for iter-2: event-driven dispatch. See iter-01/findings.md. - Q3 iter-2 (verified) — event-driven dispatch cleared the HTTP/2
layer entirely (no more PROTOCOL_ERROR). Full XPC handshake now
exchanged on both ROOT_CHANNEL (s1) and REPLY_CHANNEL (s3). CDS
closed REPLY_CHANNEL with
RST_STREAM(stream=3, error_code=5 STREAM_CLOSED)due to a dispatch-ordering bug: our table emits the 14 KB Handshake onclient DATA(s1)instead of onclient DATA(s3). Recv grew 180→217 bytes, +21%. Fix for iter-3: relocate#8/#9to theDATA(s3)trigger and splitDATA(s1)into a counter. See iter-02/findings.md. - Q3 iter-3 (verified) — dispatch table v2 reproduced the pcap
emit order exactly; all 10 replay frames dispatched cleanly (send
= 14326 B). Recv unchanged at 217 bytes; CDS still ended with
RST_STREAM(stream=3, STREAM_CLOSED). Ordering hypothesis empirically disproven — the rejection is at a deeper layer than frame scheduling. Three hypotheses remain for iter-4+: (1) drop our#9 RST_STREAM(s1), (2) unzero identifiers in#8Handshake dict, (3) introduce per-frame spacing. iter-4 tests (1) first. See iter-03/findings.md. - Q3 iter-4 (verified, closes Q3 under replay) — dropping replay
#9from theDATA(s3)trigger removed CDS's reciprocalRST_STREAM(s3): no GOAWAY, no teardown of any kind. CDS reached a quiescent state after receiving our 9-frame reply (#0–#8) and simply waits. Recv shrank 217→204 bytes — the missing 13 bytes are exactly the reciprocal RST_STREAM CDS no longer needs to send. Hypothesis 1 confirmed: our own server-initiated reset was the direct cause of iter-2/iter-3's application-layer abort. The remaining hypothesis (unzeroed identifiers) is no longer cheaply testable under replay — CDS's silence hides the signal. Pure replay has reached its ceiling. Next productive work on the s2c side is Phase D backend, which can respond dynamically to CDS's post-handshake service calls instead of pre-recording them. See iter-04/findings.md. - Phase D.5 smoke (verified, replaces Python listener with Go backend) —
Cross-compiled
cmd/iosmux-backend/for havoc's darwin/amd64, deployed in place of the iter-4 Python listener, CDS triggered. Server-side wire output is byte-exact vs iter-4 (14,313 B,cmpclean). Same 9 frames, same quiescent state. The Python spike listener is now functionally replaceable by the Go implementation built onx/net/http2.Framer+internal/xpc/codec. Next: Phase D.6 (per-service handlers, starting with whatever CDS requests afterdevicectl pair). See iter-05-go-backend/findings.md. - Phase D.6.0-B (verified, reinterprets "quiescent state") —
Deployed D.6.0-A verbose-logging Go backend on havoc, ran three
interactive
devicectltriggers (device info, manage pair, device info processes). None produced a post-handshake request: instead, CDS tore down the TCP connection with peer EOF 23-36 s after our#8 big Handshakein all three sessions. This is the natural-timeout that iter-1 through D.5 could not surface because each prior run pkill'd within ~5-13 s. Iter-4's "quiescent state" language was observation-window bias — CDS silently evaluates the handshake and disconnects, it does not "wait indefinitely". Four candidate causes ranked (H1 placeholder UUID, H2 stale Services, H3 missing keepalive, H4 UDID mismatch). Next D.6.1 tests the cheapest first. See iter-06-pair-trigger/findings.md. - Phase D.6.1-B (verified, H1 CONFIRMED STRONGLY) —
Landed UUID patching in the Go backend
(
IOSMUX_HANDSHAKE_UUID=random, commitffe7c29), redeployed to havoc, let one session run 130 s past handshake completion. Result: zero CDS-initiated EOFs, TCP ESTABLISHED for the full window, only 15-second TCP keepalives. The 16-byte zero UUID at offset0x371Cin the redacted iter-01 fixture was the sole session-progression gate. H2/H3/H4 no longer need testing. New third state observed: CDS accepts the handshake and waits silently — no disconnect (good) but also no new application traffic (still to investigate in D.6.2). The iter-4 and D.5 byte-level findings were always correct; only the UUID field blocked progression. See iter-07-uuid-patched/findings.md. - Phase D.6.2 H1c (verified, falsified) — Swapped
devicectl device infotrigger fordevicectl manage pair(and fallbackunpair+pair) against the same UUID-patched backend. Four sessions captured; every one hit the same 40-line handshake baseline with zero post-handshake frames. devicectl surface identical Mercury 1000 error as iter-6. Trigger-type is not the missing variable. D.6.3 widens pcap filter to separate H1b (CDS back-connects to advertised service ports) from H2-reply (we're missing a server follow-up frame beyond the iter-01 replay corpus). See iter-08-pair-trigger/findings.md. - Phase D.6.3 (verified, H1b FALSIFIED decisively) — Single
capture with
tcpdump -i lo0 "tcp and not port 49151"(wider than iter-8's port-filtered capture). Exactly one TCP connection observed: CDS[::1]:49492 → [::1]:34719(our backend). Zero SYNs to any of the 62 advertised service ports. CDS is NOT trying to back-connect anywhere — it's waiting for a server-initiated follow-up frame inside the same HTTP/2 stream. H2-reply is the surviving hypothesis. Next D.6.4a: temporary SPIKE→stock tunneld swap + utun4 pcap of real iPhone post-handshake flow (Q2 methodology, extended window) for authoritative reference bytes. See iter-09-wide-pcap/findings.md. - Phase D.6.4a (verified, H2-reply FALSIFIED + parallel
service discovered) — Swapped SPIKE → stock tunneld,
captured
utun4during rsd-info + lockdown info + devicectl info triggers, swapped back to SPIKE cleanly. Real iPhone greeting-stream output is byte-identical to our SPIKE backend (iter-01 10-frame corpus, falls silent after#9 RST_STREAM(s1)). H2-reply (missing follow-up frames on greeting stream) is falsified. Real discovery: a parallel non-HTTP/2 TCP service on iPhone:50367 (~9.5 KB iPhone→client, likely lockdown-over-TCP) opened by devicectl in parallel with the greeting session. SPIKE mode exposes no analogous listener — that's where CDS hangs. D.6.5 decodes the parallel service bytes and identifies what to emulate. Raw pcap held local (/home/op/backups/iosmux/pcaps/, gitignored) — contains real UDID. See iter-10-real-iphone-ref/findings.md. - Phase D.6.5 (verified, protocol identified + service
advertised) — Pure local decode of the iter-10 held-local
pcap. Protocol on iPhone:50367: classic lockdown-over-TCP
(plaintext XML plist, 4-byte BE length prefix, 3-verb API:
RSDCheckin/QueryType/GetValue). Port 50367 is advertised in our Handshake Services dict undercom.apple.mobile.lockdown.remote.trusted(UsesRemoteXPC=False, ServiceVersion=1). Full Services census: 62 services total, 40 classic-lockdown (UsesRemoteXPC=False), 22 RemoteXPC — a single lockdown handler in our Go backend unlocks 40 endpoints. D.6.6 implements the handler (bind[::1]:50367, read length-prefixed XML plists, dispatch 3 verbs, serve cached 88-key device dict forGetValue). See iter-11-lockdown-decode/findings.md. - Phase D.6.5 control (verified, back-connect hypothesis
FALSIFIED) — Re-ran iter-09 wide-pcap methodology on SPIKE
backend but with
devicectl device info detailsas trigger (the one that iter-10 proved opens lockdown against real iPhone). Expected SYN to[::1]:50367+ ECONNREFUSED. Actual: exactly one SYN, client→our-backend:34719, zero SYNs to any advertised service port, zero RSTs. CDS does NOT back-connect in SPIKE mode regardless of trigger. Causal model revised: CDS evaluates a pair/trust signal after Handshake and bails upstream of Services consultation when it reads "unpaired / untrusted" (which our SPIKE Handshake apparently signals). Important re-read of iter-10: 50367 flow there was opened by pymobiledevice3 (no trust gate), not by devicectl (has trust gate). D.6.6 plan revised: research-a (Properties dict diff) + research-b (per-field bisection patching) precede the implementation step. See iter-12-spike-control/findings.md. - Phase D.6.6-research-a (verified, H-HS narrowed) —
Byte-diffed the 46-key Handshake Properties sub-dict
between the SPIKE fixture and the real iPhone reference
pcap. 36 keys bit-exact same, 5 zeroed-by-us (declared
redactions: UDID / Serial / MAC / ChipID / BootSessionUUID),
5 differ (all iOS 26.4.1→26.4.2 point-version drift,
not gate candidates), 0 only-ours / only-real. Dict
shape, key set, and insertion order are all identical.
Consequence: iter-12's speculative H-HS candidates
(
HostAttached,PairRecords,TrustedHostAttached,ActivationState,PasswordProtected) are not in Properties at all — they live in the GetValue response on the classic-lockdown port :50367 that CDS never reaches in SPIKE. H-HS narrows to a 5-slot field-bisection over the identity redactions, UDID most likely gate by a wide margin. D.6.6-research-b is now a concrete patch order: UDID → Serial → ECID → BootSessionUUID → MAC, stopping on first SYN to[::1]:50367. See iter-13-hs-props-diff/findings.md. - Phase D.6.6-research-b iter-14 (verified, UDID-alone
FALSIFIED) — Added env-gated UDID patching
(
IOSMUX_HANDSHAKE_UDIDat fixture offset 0x34BC, same mechanism as D.6.1's UUID patch at 0x371C). Ran the iter-12 trigger across 4 runs. Handshake carries the real tunneld-advertised UDID (confirmed in verbose log every run), devicectl reportspairingState: unpaired+ Mercury warning (same as iter-12), 0 SYN to any advertised service port in a 30-second window. H-HS narrowed slot #1 refuted. Interesting positive signal: CDS closes the session ~3 s after Handshake (vs iter-12's ~30 s with zero UDID) — UDID IS being consumed, just triggering faster rejection rather than progression. Methodology note surfaced: tunneld hardware UDID (25-char, goes into Handshake) ≠ CoreDevice UUID (36-char, goes intodevicectl --device); iter-12's placeholder meant CoreDevice UUID. Next options: combined UDID + Serial + ECID patch (iter-15), or pivot to host-side pair-state investigation. See iter-14-udid-patch/findings.md. - Phase D.6.6-research-b iter-15 (verified, H-HS multi-field
FALSIFIED) — Scaffolded env-gated patching for SerialNumber
(offset 0x3364, 10 B ASCII) and UniqueChipID / ECID (offset
0x34EC, 8 B LE uint64; initial guess of 0x34EA was off by 2
bytes — our own verbose decoder caught it with "unknown type code
0x401e4000" after the patch corrupted the low bytes of the XPC
uint64 type tag — fix in
2bcea98). Fetched real identity values viapymobiledevice3 remote rsd-info --tunnel <UDID>in a brief stock-tunneld window. Ran iter-12 trigger across two clean post-fix runs with all 3 Properties identity fields (UDID + Serial + ECID) matching tunneld's advertisement. Handshake parses cleanly on CDS side (no RST_STREAM), devicectl still reportspairingState: unpaired+ Mercury warning, 0 SYN to any advertised service port in a 30-s window. Most important signal: close-timing reverts from iter-14's ~3 s to iter-12's ~30 s. If the gate were wire-level identity matching, iter-15 should have either unblocked (SYN to :50367) or closed fast like iter-14. Neither happened. H-HS dead at both single-field (iter-14) and multi-field (iter-15) levels. H-PairState promoted to primary working hypothesis: the gate reads host-side state (CoreDevice paired-devices DB / lockdown escrow) that wire-state patches cannot influence. Next step: iter-16 read-only pass on/var/db/lockdown/<UDID>.plist, CoreDevice DB, keychain entries — then iter-17 decide whether pair-record plant is feasible without corrupting live iPhone usage. Apparatus lesson surfaced across iter-15's 5 runs: CoreDevice registry is fragile and decays within ~5 min after the first SPIKE-mode experiment per VM reboot — reproducibility runs must be collected back-to-back in the same warm window. See iter-15-multifield-patch/findings.md. - Phase D.6.6-research-c iter-16 (verified, H-PairState localized)
— Read-only filesystem, SQLite, keychain,
log show, andfs_usage(SIP-disabled) survey of havoc's pair-state surface. The authoritative pair store is/var/db/lockdown/RemotePairing/user_{UID}/peers/<CoreDevice-UUID>.plist— an NSKeyedArchiverCUPairedPeerrecord keyed by the 36-char CoreDevice UUID, holding a 32-B Ed25519 public key (pk), a 16-B BT IRK (altIRK), anidentNSUUID, and device metadata. The host's own identity lives in the siblingidentity.plistwith public + PRIVATE Ed25519 keys stored inline in the plist (significant iOS 17+ design shift from the classic keychain-held identity model). CoreDevice's on-disk SQLite (~/Library/Developer/CoreDevice/Devices/db.sqlite) is a cache with 0 data rows. System keychain has 0 iPhone/RemotePair entries. BT-LE paired-device DB has 0 rows. The gate:remotepairingdloads peer records into memory at startup and matches live Bonjour_remotepairing._tcp.localadverts against them. Neither pymobiledevice3 tunneld nor SPIKE backend publishes that mDNS service, so no advert ever resolves → every advert taggedunauth device→ CoreDevice reportspairingState: unpaired. The trust gate is below RSD and requires: (a) a matching mDNS advert, AND (b) a successful Ed25519 challenge-response the device signs with its own secure-enclave-held private key. iter-17 minimal test: plant a fake peer + fake mDNS advert, kickstartremotepairingd, check whether metadata-query pairingState flips independently of the cryptographic handshake. Backup-first discipline mandatory. See iter-16-pairstate-survey/findings.md. - Phase D.6.6 iter-17/18 (verified,
xcrun devicectl manage pairFALSIFIED + bridge-not-proxy architecture confirmed) — iter-17 attempted the iter-16 plant-and-advert minimal test; abandoned mid-flight when remotepairingd rejected the planted advert (Ignoring bonjour advert without expected TXT record keys: Apple expectsidentifier=/authTag=/ver=/minVer=/flags=, the planted one usedident=/id=/uuid=/udid=) andauthTagwas discovered to rotate every ~8 s alongside the rotating identifier (iOS 17+ privacy), making static planted adverts non-viable without first solving the authTag derivation. iter-18 then took the obvious next step: drive the documented Apple pair flowxcrun devicectl manage pair --device <CoreDevice-UUID>. Two runs, both fail in ~2 s before any Trust-this-computer dialog can appear on the iPhone. Run A (active 150 KB inject, sha26ca1658):kAMDNotConnectedError/ Mercury 1000 — CDS finds the synthetic device the inject registers but cannot reach the iPhone-sidenotification_proxyservice through the synthetic device's tunnel. Run B (96 KB.disabledstub, sha97a8cfae, swapped into the active load-path):Connection refused 127.0.0.1:62078— synthetic device disappears fromdevicectl list devicesentirely (No devices found), CDS falls back to classic usbmuxd forward path, no listener. The 96 KB variant is also not a no-op stub — its own log line[iosmux] FATAL: XPC proxy init failedconfirms it tries to bring up its own plumbing and crashes; symbol-table diff (267 vs 411 symbols,__text0x7a10 vs 0xf13b) shows it lacks the SPIKE / RSD hook surface but still installs the Objective-CIosmuxMDProxyclass. Combined outcome re-confirms the bridge-not-proxy architecture already documented inconnection.md"Phase S2.B note" andstage2.md"Phase B realization": iosmux synthesises an iOS device inside the Mac VM's CoreDevice subsystem; it is not a transparent proxy of the physical iPhone. Pair-state, trust, and the classic-lockdown surface are owned by the iosmux Option δ shim, not derived from a successful iOS Pair-Setup against the real device. Phase D.6.6-impl remains the next concrete step (Go backend binds[::1]:50367, serves the three-verb classic-lockdown protocol, declares the synthetic device's pair-state from a captured empirical source — exact mechanics still being researched). Future iters do not retryxcrun devicectl manage pair, mDNS-advert planting, or authTag derivation. See iter-18-devicectl-pair-falsified/findings.md. - Q4 (verified) — pymobiledevice3 has no "pretend to be iPhone"
server fixture, but its
XpcWrapper.build+_build_xpc_*helpers are direction-agnostic and reusable for server-side replies. See Q4-server-side.md. - Q5 (closed without work) — the post-TLS hook was conditional on Q2 showing TLS. Q2 showed plaintext. No TLS means no key-material extraction, no recv() hook. See Q5-post-tls.md.
Every answer is tied to a raw artifact or a direct pymobiledevice3
source line. Named residual gaps (flag bit 0x200 without an enum
name, and the message_id=1 skip) are small and non-blocking for
Phase D.
Language discipline for Phase D¶
The listener in this directory is written in Python because
it is one-shot research tooling, not because the backend will
be Python. Phase D's production backend is Go. Any new
production code for Stage S2's Option δ shim lands in
cmd/iosmux-backend/ and uses:
golang.org/x/net/http2for prior-knowledge h2c (via theFramerinterface directly, because RemoteXPC does not fitnet/http.Handlersemantics)- a hand-written XpcWrapper / XpcPayload codec ported from
pymobiledevice3's
remote/xpc_message.pyas reference howett.net/plistfor bplist decoding (extended if bplist16 is not already covered)
The only Python in steady state remains the existing
pymobiledevice3 remote tunneld subprocess, which Go talks to
over its HTTP API on loopback. No new long-lived Python
processes get introduced by Stage S2.
When Stage S2 moves from research into implementation, this
entire directory goes away alongside
../../../patches/pymobiledevice3/
and the inject additions in iosmux_wire_logger.m.