Skip to content

ADR-0009 — iosmux is a bridge that synthesises an iOS device; native iOS Pair-Setup against the real device is out of scope

Status

accepted

(This ADR was first filed in commit 74363ac and immediately removed in commit 404b9e0 because the original draft only restated facts already in connection.md and stage2.md. The present version is filed because the underlying architectural commitment IS still load-bearing and IS still uncodified — but only after iter-12, iter-16, and iter-18 produced the empirical evidence that warrants an ADR. The value this ADR adds over the existing connection.md / stage2.md text is the explicit rejection of three concrete candidate paths that were repeatedly re-litigated across sessions.)

Context

By session 11 (2026-04-27) the project had cycled through three candidate paths for getting the synthetic device's pairingState from unpaired to paired. Each was tried and each is grounded in specific empirical evidence in docs/research/protocol/:

  • Option A — force-write pairingState=.paired on the synthetic DeviceInfo from inside the inject. Removed in Stage S2.A (commit 18331ca) as "lying about device state". Re-proposed multiple times since; each re-proposal disregarded the S2.A rationale documented in inject/iosmux_inject.m around the state_setter block. The user has explicitly rejected this path in session 11.
  • Option B — let CoreDeviceService run a real RemoteXPC pair-setup ceremony against the synthetic device. This presupposes that CDS will open an nw_connection for pair-flow that we can serve from a local backend. iter-09 (D.6.3) and iter-12 (D.6.5-control) empirically showed CDS opens zero SYNs to any service port in SPIKE mode regardless of trigger; the trust gate fires upstream of the connection. iter-13/14/15 falsified every wire-level Handshake Properties identity-field bisection (UDID alone, then UDID+Serial+ECID together). iter-16 localised the gate to remotepairingd's in-memory CUPairedPeer registry matched against live _remotepairing._tcp.local Bonjour adverts plus an Ed25519 challenge-response anchored in the iPhone's secure enclave. iter-17 attempted to plant a fake Bonjour advert and was abandoned mid-flight (wrong TXT keys; rotating authTag is a keyed hash with non-extractable key material). iter-18 ran xcrun devicectl manage pair end-to-end and the iPhone-side Trust dialog never appeared because CDS bailed before reaching any iOS Pair-Setup endpoint.
  • Option C — synthetic device owns its own pair-state surface; iosmux backend serves the lockdown-over-TCP :50367 advertised in our Handshake under com.apple.mobile.lockdown.remote.trusted, plus a Mercury Codable interceptor for static-success actions (AcquireDeviceUsageAssertion above all). All RSD service connections Xcode actually needs (install / DDI / debug / app-launch) flow through the existing remote_service_create_connected_socket interpose chain in inject/iosmux_inject.m and onto the iPhone via the pymobiledevice3-tunneld utun. iter-11 (D.6.5) specified the protocol (RSDCheckin / QueryType / GetValue, 3 verbs). iter-18 explicitly named this as "the only remaining way for xcrun devicectl manage pair to succeed against a device routed through this VM" (iter-18-devicectl-pair-falsified/findings.md §"Why this falsifies 'transparent proxy' architecture").

The session-11 research agent's evidence matrix is in docs/research/scratch/session11-b-vs-c-and-doc-gaps.md (temporary; not committed long-term). It cites file:line evidence for every claim above.

Decision

iosmux is a bridge: it stands up a synthetic iOS device inside the macOS VM's CoreDevice subsystem and serves every host-side trust / pair-state / lockdown surface for that synthetic device itself. iosmux does not orchestrate native iOS Pair-Setup against the physical iPhone on behalf of the macOS VM, and Xcode is not expected to drive a Pair-Setup ceremony that would touch the device's secure enclave.

The three candidate paths are each given a final disposition:

  • Option A is rejected. No DeviceInfo-field force-writes that bypass the natural code path. Stage S2.A's removal stands.
  • Option B is rejected. No attempts to make CDS run a native RemoteXPC or RemotePairing pair ceremony against the synthetic device, no Bonjour-advert planting, no authTag derivation, no Ed25519 challenge synthesis. The cryptographic gate is held by the iPhone's secure enclave; iosmux cannot satisfy it from the Mac VM and is not expected to.
  • Option C is the path. Phase D.6.6 implementation work (lockdown handler + Mercury Codable interceptor + existing service-socket interpose chain) is the canonical forward direction. See stage2.md Phase D.6.6 for the work plan.

Consequences

Wanted

  • Future sessions stop re-litigating the same three options. Any proposal to re-introduce force-writes, run native Pair-Setup, or plant Bonjour adverts is rejected by reference to this ADR with no further argument required.
  • Phase D.6.6's scope is bounded: lockdown 3-verb handler + Mercury Codable interceptor for the small "static-success" action set (Pair, Unpair, AcquireDeviceUsageAssertion, ListUsageAssertions, Connect, Disconnect, Tags, GetTrainName, DarwinNotificationObserve, DarwinNotificationPost, EnableDDIServices, DisableDDIServicesAction, FetchDDIMetadata, UpdateHostDDIs, RemoveHostDDIs) and the existing RSD service-socket interpose chain handles everything else (AppInstall, CreateServiceSocket, TransferFiles, ReceiveFiles, RsyncFiles, ListFiles, FetchDyldSharedCacheFiles, FetchMachoDylibs, FileNodeDetails, plus debug-attach via service-socket open).
  • The active iosmux_inject.dylib remains load-bearing — the patched CoreDeviceService has LC_LOAD_DYLIB on /Library/Developer/CoreDevice/iosmux_inject.dylib and that file's presence is required for CDS to launch (verified in iter-18 Run B: stub disabled the synthetic device entirely and devicectl list devices reported "No devices found").

Accepted as cost

  • Operations that genuinely require iPhone-side Mac-identity enrolment (first-time provisioning of a new development machine against an iPhone whose secure enclave is the source of truth) remain out of scope. The user has stated repeatedly that the iPhone is read-only with respect to iosmux, so this is not a loss.
  • The Mercury Codable Output byte layout for at least AcquireDeviceUsageAssertionActionDeclaration.Output and the other static-success actions is currently undocumented; D.6.6 implementation work has to capture this before encoding any reply. Tracked as a documented work item in stage2.md Phase D.6.6.
  • The 88-key GetValue lockdown dictionary content lives in a held-local pcap (/home/op/backups/iosmux/pcaps/, gitignored); D.6.6 needs the dict committed as a redacted fixture or re-captured at backend startup against stock-tunneld. Tracked similarly.

Evidence

References

  • ADR-0001 — pymobiledevice3 as the iOS-side protocol source-of-truth.
  • ADR-0002 — tunneld inside the VM, which is what makes a VM-side synthetic device feasible.
  • ADR-0004 — Option δ as the canonical Stage 2 architecture; this ADR clarifies what \"the shim's responsibilities\" actually cover end-to-end.
  • ADR-0005 — Go-only backend; this ADR's Option C work lands inside cmd/iosmux-backend/.
  • ADR-0006 — empirical-only discipline; the iter-13/14/15 falsifications cited above are the discipline applied.
  • docs/architecture/connection.md "Phase S2.B note" and docs/plans/stage2.md "Phase B realization" already framed this architecturally; this ADR adds the formal rejection of Options A and B alongside the affirmation of Option C, which was previously implicit.