Documentation¶
Status: verified — session 10, 2026-04-13
Every claim on this page links to either a delivered commit or a research artifact with a raw-data backing. No guesses.
Live site
This site renders from the main branch of the private
staticwire/iosmux repository and is deployed to Cloudflare
Pages at https://iosmux-docs.pages.dev/. Every push to
main that touches docs/ or mkdocs.yml triggers a rebuild
via .github/workflows/docs.yml.
How to read these docs¶
This tree is organized so that what we know is separated from what we are planning and from what we are investigating. Every document belongs to exactly one of these four categories and carries a status badge at the top.
The four trees¶
architecture/— how iosmux works right now. Ground-truth explanations. Edit only when the code actually changes.plans/— forward-looking work. Stage plans, queues, acceptance criteria. Superseded plans move toplans/archive/with a date-prefixed filename.research/— empirical work, grouped by topic (protocol / coredevice-internals / environment). Every claim is backed by a raw artifact or a direct upstream-source quote. No guessing — anything open is named as a gap and tracked in the relevant plan.adr/— Architecture Decision Records. Immutable. New decision → new ADR; an old ADR is never edited, only superseded.
Plus patches/ for the
pymobiledevice3 upstream-plus-overlay workflow and
runbooks/ for operational how-tos.
Status badges (YAML frontmatter)¶
Every key file starts with a status: frontmatter block. Readers
should treat the status as the load-bearing metadata, not the age of
the document.
| Status | Meaning |
|---|---|
verified |
Empirically confirmed against a raw artifact, a primary source, or a landed commit. Strongest claim. |
reviewed |
Derived logically from verified facts. Ran through a review pass. Safe to act on but not itself raw evidence. |
draft |
In progress — expect churn. |
hypothesis |
Unconfirmed, needs an empirical task to become verified or be dropped. |
superseded |
Content replaced by another document. Frontmatter includes superseded-by: pointing at the replacement. Kept for history. |
archived |
Historical. No longer active, no longer maintained, but preserved for future context. |
Inline admonitions¶
On top of the standard MkDocs Material admonitions (note, warning,
info, danger, …), four custom admonition types are available for
inline evidence calls. They share the status vocabulary above:
!!! verified "..."— wraps a specific empirical claim with its evidence. Green. Use when a sentence would otherwise feel unsourced.!!! hypothesis "..."— wraps an unconfirmed assumption. Yellow. Must name the research task that would confirm or refute it.!!! gap "..."— wraps a known empirical unknown. Red. Halts any downstream decision that would require the missing data.!!! superseded "..."— wraps a claim that has been replaced, pointing at the replacement. Grey.
The hard rule¶
No guessing. Every claim is either backed by evidence we can point at, or explicitly labeled as a gap. When research bisects a protocol one step at a time, we never reply to the other side with a byte sequence we cannot source from a capture or a primary reference — the bisection stops at the first unknown and the unknown is named.
This rule is formalized in ADR-0006 (Phase 4 deliverable) and is why the Stage 2 / Phase C self-research queue explicitly halts on gaps rather than proposing plausible-but-invented replies.
Current state (session 10, post S2.A + S2.B + S2.C)¶
Stage S1 (A, B, C, D) has landed. devicectl list devices returns
the virtual iPhone end-to-end on the very first call after a fresh
CoreDeviceService launch. Xcode's Devices window shows the device
row with an honest Pair button and the state=connected (no DDI)
label CDS produces on its own when we stop lying about device
state. The L2 bridge + VM-local pymobiledevice3 tunneld + CDS
inject chain is documented in
architecture-connection.md.
Stage S2.A removed four DeviceInfo force-writes that had been
bypassing the pair flow (commit 18331ca). Only
DeviceInfo.state=connected was kept, because it is a truthful
link-state value (we hold a live RSDDeviceWrapper with a working
tunnel), and removing it caused Xcode to filter the device out.
Stage S2.B ran the Pair-button smoke test and produced a
course-correcting finding: CDS cannot natively speak the transport
that pymobiledevice3 tunneld exposes. Clicking Pair causes CDS to
open nw_connection directly to the tunnel RSD endpoint and
receive an immediate TCP RST. This killed Options α/γ from the
original Stage 2 plan and moved the project to Option δ:
intercept CDS's outgoing connection and route it through a local
shim backend that speaks the protocol CDS expects. Full analysis in
s2b-pair-attempt-log.md.
Stage S2.C ran a single unified self-experiment on havoc that closed the three blockers from the devil's-advocate pass:
- X (client-side protocol) — captured and decoded. CDS speaks
plain prior-knowledge HTTP/2 cleartext, no TLS, no ALPN.
Above HTTP/2 it uses RemoteXPC DATA frames on stream 1
(ROOT_CHANNEL) and stream 3 (REPLY_CHANNEL) with XpcWrapper /
XpcPayload framing whose magic bytes match
pymobiledevice3/remote/xpc_message.pybyte for byte. A Go HTTP/2 server can serve this. - Y (which PID) — empirically the same PID our
LC_LOAD_DYLIBinject loads into. No helper process, noremotepairingddelegation. The earlier "three CDS PIDs" observation was concurrent launchd instances, not a split of responsibilities. - Z (sandbox) — all four Shape B primitives (AF_UNIX socket,
socketpair, bind to /tmp/*.sock, sendmsg with SCM_RIGHTS) work
inside CoreDeviceService's sandbox with
errno=0.
Full hex-level write-up in s2c-self-experiment/FINDINGS.md. Shape B is therefore architecturally viable; Phase D is a Go implementation with pymobiledevice3 staying confined to the existing tunneld subprocess.
Phase D direction is Go-only. The Option δ backend is a new
Go component under cmd/iosmux-backend/ that uses
golang.org/x/net/http2 for h2c + a hand-written XpcWrapper /
XpcPayload codec ported from pymobiledevice3 as reference. No new
production Python code is introduced; the existing tunneld Python
subprocess is the only Python in steady state, and we talk to it
as an HTTP client on loopback as we do today.
Active plans¶
- plan-stage2-pair-flow.md —
authoritative forward plan. Phase A (revert DeviceInfo
lies), Phase B (smoke test), and Phase C self-experiment have
landed. Blockers X / Y / Z are closed. Phase C still has a
self-research queue (Q1-Q5) to fully characterize the server
side of the CDS ↔ iPhone protocol before implementation
begins. Phase D is the Go-only Option δ backend
(
cmd/iosmux-backend/); Phase E covers downstream (DDI / developer mode / install / debug) through the same backend. - docs/patches/pymobiledevice3/ —
temporary upstream-plus-patches overlay for pymobiledevice3.
Every patch is documented with a
Go-Rewrite-Noteso the Phase D Go backend can drop Python at the first opportunity without losing any behavior. Deleted when Python is removed. - plan-stage1-rebuild.md — the session 8-9 roadmap that delivered S1. Kept as historical reference.
- architecture-connection.md — ground-truth architecture diagram of how iPhone, host, VM, tunneld, and CDS inject fit together. Includes a "historical lesson" section on why host-side tunneld was the wrong architecture the first time, so future sessions do not re-introduce it.
Stages landed so far¶
Stage 0 — Safety-belt fixes (f529798)¶
Five defensive fixes from docs/research/code-audit-findings.md
(C1, C2, C4, C5, C6). Removed a class of latent memory/ABI bugs in
the inject that had been surviving by accident. No behavior change,
but subsequent stages could not have landed safely without these.
Stage S1.A — Properties from in-memory handshake (c89dbe1)¶
The inject used to HTTP-fetch device properties from a Go relay
running on port 62078. The fetch was a hard dependency on the
relay being up at exactly the right moment; when it was not, the
whole CDS device-registration chain cascade-failed. Replaced the
HTTP fetch with a direct read of g_rsd_handshake_properties,
which is already in process memory at that point. No more relay
dependency, no more Go-relay HTTP path in steady state.
Stage S1.B — Passthrough trampoline for Step 19 (8197c8a)¶
Step 19 was an inline hook installed at CoreDevice + 0xB896 that
replaced invoke(anyOf:usingContentsOf:) with mov al, 1; ret.
It was added in session 5 to mask a SIGSEGV in PairAction's NULL
self handler, but the SIGSEGV's root cause was the SDR
read-overflow fixed in session 6, not anything action-dispatch
related. With the overflow gone, the mov al, 1; ret stub was
silently eating DeviceManagerCheckInRequest by returning "handled"
for every message that reached Mercury. Converted to a passthrough
trampoline (movabs r10, orig; jmp r10) so the original
invoke(anyOf:) runs, check-in reaches ServiceDeviceManager,
and Published DeviceManagerCheckInCompleteEvent fires for the
first time since session 5.
Stage S1.C — DeviceIdentifier enum tag (7e7a403)¶
Smoking-gun one-byte bug. The inject was writing dev_id_buf[32] = 0
with a comment labelling that byte as "enum tag = UUID variant".
The comment was wrong. CoreDevice.DeviceIdentifier is:
enum DeviceIdentifier {
case ecid(UInt64) // tag 0
case uuid(Foundation.UUID, Swift.String) // tag 1
}
With tag = 0, CDS was interpreting the first 8 bytes of our UUID
buffer as a UInt64 ECID. Downstream, uuidRepresentation.getter
fell into an AMDevice keypath synthesis path that produced a
completely different UUID, and every identity-keyed lookup inside
CDS ended up querying under that synthetic UUID instead of our
configured one. Fix: set tag = 1 and populate the
.uuid(UUID, Swift.String) payload correctly (16-byte UUID + empty
small-string marker 0xE000000000000000 + tag byte). After this
one-byte change the whole identity chain became consistent.
The same commit also deleted the dead
updateIdentifier(_:forIdentityManagedDevice:) call that was
previously attempted as an SDR-registration path. Runtime research
had already shown the call was hitting the miss branch with no
side effects; it was not part of the real registration path.
Stage S1.D — Hook install(browser:) (54edb58)¶
The inject constructor used to schedule the main registration work
via dispatch_after(3 * NSEC_PER_SEC). That was a day-one guess
about "how long until CDS has a live ServiceDeviceManager". On
fresh launches it lost the race — the first check-in request
arrived and was served from an empty managedServiceDevices dict.
Replaced with an inline hook on
ServiceDeviceManager.install(browser:) at
CoreDevice + 0x27d7d0. The hook captures the SDM self pointer
on the first call (Swift class-method ABI puts self in %r13),
swift_retains it, and dispatch_asyncs the registration onto a
dedicated serial queue. No more wall-clock guessing; the signal
comes from CDS's own init chain.
Five parallel risk-research agents validated the approach before
any code was written: prologue patchability, thread context,
swift_retain from C, deadlock surface, symbol stability. All
came back green. Hook target was also revised from
__allocating_init to install(browser:) mid-research because
the latter gives us the SDM pointer AND the guarantee that SDM is
in a fully-constructed state.
Restore script (0eb972f)¶
scripts/iosmux-restore.sh is the single idempotent post-reboot
script. It drives the host bridge (iosmux bridge, sudo for
sysfs / ip link), starts a fresh pymobiledevice3 remote tunneld
inside the VM via ssh havoc-root (so no sudo inside the VM), and
waits until the tunnel API reports a live device. Always tears
down any existing tunneld and rebuilds, so the post-run state is
deterministic regardless of what was already running.
Stage S2.A — Stop lying about device state (18331ca)¶
Removed three DeviceInfo force-writes that were bypassing the
real pair flow: pairingState = .paired,
preparednessState = .all, and
areDeveloperDiskImageServicesAvailable = true. The fourth
candidate (state = .connected) was temporarily removed and then
restored after empirical test showed Xcode filters devices whose
DeviceInfo.state defaults to .unavailable — the link
physically exists, so .connected is a truthful link-state value,
not a lie about pairing. visibilityClass=default was kept as a
UI categorization, not a lifecycle field.
Stage S2.B — Pair-button smoke test and course correction¶
Not a code commit — an empirical finding that changed the Stage 2
plan. With the three lies removed, clicking Pair in Xcode caused
CDS to open nw_connection directly to the tunnel RSD endpoint and
receive an immediate TCP RST. The tunnel is fully functional via
pymobiledevice3 Python clients (rsd-info, mounter list, DVT
filesystem listing all verified working). The impedance mismatch
is inside the VM, between Apple's native CDS client and
pymobiledevice3's tunneld transport. Option α ("let CDS speak to
the tunnel naturally") is falsified; Option δ (interpose CDS and
redirect to a local pymobiledevice3 backend) is the new direction.
Full analysis in
research/s2b-pair-attempt-log.md.
Stage S2.C — Self-experiment: three blockers closed (afd8dd6 + this commit)¶
A single unified empirical run on havoc that resolved the three Stage 2 blockers identified by the devil's advocate:
- Blocker X (client-side protocol) — CDS speaks plain
prior-knowledge HTTP/2 cleartext (h2c), no TLS, no ALPN.
Captured with a spike HTTP/2 listener on
[::1]:34719(advertised to CDS via anIOSMUX_SPIKE=1-gated pymobiledevice3 tunneld patch). First DATA frame on stream 1 carries an XpcWrapper / XpcPayload blob whose magic bytes (0x290bb092,0x42133742) match pymobiledevice3'sremote/xpc_message.pybyte for byte. Go'sgolang.org/x/net/http2.Framercan serve this directly. - Blocker Y (which PID) — every
nw_connection_*call logged by the newiosmux_wire_logger.mDYLD_INTERPOSE bank came from the same PID ourLC_LOAD_DYLIBinject loads into. No helper process, noremotepairingddelegation. Current injection is sufficient. - Blocker Z (sandbox) — a runtime syscall probe added to the
inject constructor ran
socket(AF_UNIX),socketpair(),bind()to/tmp/iosmux_probe_*.sock, andsendmsg()withSCM_RIGHTS. All four returnederrno=0. Shape B's fd-passing is sandbox-legal.
Full hex breakdown and raw artifacts in
research/s2c-self-experiment/FINDINGS.md.
Phase D is now unblocked and committed to a Go implementation —
cmd/iosmux-backend/ driven by golang.org/x/net/http2, with a
hand-ported Go XpcWrapper / XpcPayload codec using pymobiledevice3
as a reference source. No new production Python.
Session 10 realizations — bypass audit + transport audit¶
Two course corrections happened in session 10. The full story lives
in plan-stage2-pair-flow.md; short versions follow.
Bypass audit (pre-S2.A). S1 finished with devicectl list
devices working, but the very next smoke test revealed that
several Stage-1 decisions had been made in bypass mode: four
DeviceInfo fields were force-written to make CDS believe the
device was further along its lifecycle than it really was
(pairingState=.paired, preparednessState=.all,
areDeveloperDiskImageServicesAvailable=true, and state=.connected
at a stage where CDS had not yet observed a transport). Plus an
earlier proposal from the same session (intercept
acquireUsageAssertion to make the Pair button disappear) was in
the same bypass family. All removed from planning. Stage S2.A
reverted the three truly-fake fields; state=.connected was kept
on second pass because the link physically exists and the value is
truthful.
Transport audit (S2.B). With the inject honest, clicking Pair
produced an immediate TCP RST on CDS's nw_connection to the
tunnel RSD endpoint — not a crash, not a lie CDS was checking, not
a missing DeviceInfo field. CDS simply cannot speak
pymobiledevice3 tunneld's transport. The earlier assumption that
"a real pair flow over the real tunnel will eventually succeed"
was itself a bypass of the transport-mismatch reality. The
forward direction is no longer "let the real pair flow run
naturally"; it is Option δ: interpose CDS's service-open call
chain and redirect it to a local pymobiledevice3 backend that
already speaks the right transport, translating Python-side results
back into the shapes CDS expects. This generalizes the pattern
our MDRemoteServiceSupport interposers already use for property
queries.
Research index¶
Session 10¶
- s2b-pair-attempt-log.md —
Phase B deliverable. Full capture of the Pair-button smoke test:
CDS's
nw_connectionto the tunnel RSD endpoint, the immediate TCP RST, the ground-truth verification that pymobiledevice3 clients work on the same tunnel, and the falsification of Options α/γ in favor of Option δ. - s2c-self-experiment/FINDINGS.md
— Phase C self-experiment deliverable. Hex-level breakdown of
the captured CDS → spike-endpoint bytes (HTTP/2 preface +
SETTINGS + empty-headers stream opens + XpcWrapper DATA
frames), the per-PID wire-log from the new
iosmux_wire_logger.minterposers, and the sandbox syscall probe results. Three blockers (X / Y / Z) resolved, Shape B declared viable, Go pivot committed. Raw artifacts in theresults/sibling directory. - s2c-self-experiment/README.md — runbook for the self-experiment (install patched pymobiledevice3, deploy inject, start listener, trigger CDS, collect artifacts). Still useful as the entry point for any future spike run against a different CDS build.
Session 9 (delivered S1)¶
- session8-five-questions.md — the Q1..Q5 synthesis from session 8 that seeded the rebuild.
- s1a-properties-audit.md — properties source compatibility audit for S1.A.
- s1c-managed-service-devices-registration.md
— first-pass research on how SDRs enter
managedServiceDevices. Superseded by the disasm doc below after runtime verification. - s1c-static-browser-disasm.md — deep disasm of the registration chain, the SDM install browser path, and the Q-A..Q-F clarifying investigations that produced the Q-C enum tag finding. This is the heaviest single research doc; future sessions should grep it before disassembling anything in CoreDevice.
Session 8 (consolidation + preparation)¶
- rsd-info-ios2641-reference.json
— captured
pymobiledevice3 remote rsd-infooutput. Reference for which property keys actually come off the iPhone vs. which we synthesize.
Earlier sessions (historical)¶
- action-interception-full-picture.md — early action-dispatch research. Flagged in multiple later sessions as containing bypass-oriented conclusions; read with skepticism. Parts of it remain useful for understanding CoreDeviceUtilities internals.
- coredevice-injection.md — general CDS dylib injection mechanics.
- coredevice-representation.md
—
ServiceDeviceRepresentation/ServiceDeviceManagerdata structures as we understood them pre-S1. - coredevice-xpc-protocol.md — Mercury XPC dispatch, action identifiers, device discovery pipeline.
- os-remote-device-api.md — OS_remote_device C API, ivar layout, connection handling.
- rsd-wrapper-init-analysis.md
—
RSDDeviceWrapper.initcall chain analysis. - pair-button-and-cfnetwork.md
— the
_shadowUseAssertion/hasConnectionresearch that seeded theacquireUsageAssertionbypass idea we later rejected in session 10. Useful for the Xcode-side architecture; skip the "make Pair button disappear" prescriptions. - remotexpc-protocol.md — RemoteXPC byte-level protocol.
- l2-bridge.md — L2 bridge architecture.
- code-audit-findings.md — the Stage 0 audit that produced the six safety-belt fixes.
- phase2-wrapper-fix-test-results.md and phase2-behavior-analysis.md — test records from the wrapper read-overflow fix deployment.
Other plans¶
- plan-forward-roadmap.md — superseded
by
plan-stage1-rebuild.mdand thenplan-stage2-pair-flow.md. Kept for history. - plan-full-xcode-integration.md — early architecture overview.
- plan-cds-rsd-inject.md — CDS inject implementation details and open issues from before S1.
- plan-next-steps.md — historical snapshot of workstream progress from sessions 5-6.