Skip to content

Phase D.6.6 iter-17/18 findings — xcrun devicectl manage pair against the iosmux synthetic device is falsified end-to-end; bridge-not-proxy architecture confirmed

Status: verified — 2026-04-27

Two pair-flow attempts run on the havoc Mac VM against the live iPhone (USB-NCM-tunnelled from the Linux host). Run A: with the full iosmux_inject.dylib (150,112 B, sha 26ca1658…) loaded into CoreDeviceService — instant kAMDNotConnectedError / Mercury 1000, no iPhone-side Trust UI. Run B: same command against the smaller iosmux_inject.dylib.disabled (95,904 B, sha 97a8cfae…) swapped into the active path — Connection refused 127.0.0.1:62078 from CDS, the synthetic iPhone (iosmux) disappears from devicectl list devices entirely (No devices found), and the stub injects' own log line (iosmux_inject.dylib) [iosmux] FATAL: XPC proxy init failed confirms the smaller dylib is not a no-op stub but a different functional variant. Both runs re-confirm the Phase S2.B / Stage S2 finding that iosmux is a bridge that synthesises a device, not a transparent proxy of the physical iPhone (already documented in connection.md and stage2.md "Phase B realization"; this iter only adds empirical reinforcement).

TL;DR

iter-16 localised the trust gate to remotepairingd's in-memory peer table matched against live _remotepairing._tcp.local Bonjour adverts (see iter-16-pairstate-survey/findings.md). iter-17 was the first write-side probe: plant a fake peer record, publish a matching mDNS advert, see whether pairingState flips for metadata-level queries before the Ed25519 challenge gate runs.

iter-17 was abandoned mid-flight after the planted advert was rejected by remotepairingd with Ignoring bonjour advert without expected TXT record keys — Apple expects identifier=/authTag=/ver=/minVer=/flags=, the planted advert used ident=/id=/uuid=/udid=. authTag was also discovered to rotate every ~8 s alongside the rotating identifier (privacy feature in iOS 17+), so a static planted advert cannot match even with corrected key names without solving authTag derivation first. The planted artifacts (one E269C66F-…plist peer record and one dns-sd registration) were captured to backups and cleaned up at the start of iter-18.

iter-18 then took the obvious next step: stop trying to fake the native pair-state machinery and just run xcrun devicectl manage pair --device <CoreDevice-UUID>, the documented Apple way to drive a first-time pair. With the user prepared to tap "Trust This Computer" on the iPhone the moment the dialog appeared, two runs were attempted under different inject loadouts. Both failed in two-second windows; the iPhone-side Trust dialog never appeared because CDS bailed before reaching the iOS Pair-Setup endpoint.

The dual failure re-confirms the architectural finding already documented in connection.md and stage2.md "Phase B realization": native CoreDevice pair flow against the iosmux synthetic device is not the path to Xcode integration. The path is to extend the iosmux Go backend to be the lockdown / pair-state authority for the synthetic device itself — the Option δ shim, scoped by Phase D.6.6.

Apparatus state going into iter-17/18

  • iPhone iOS 26.4.x on USB-NCM bridge from Linux host into Mac VM havoc-vm. Real hardware UDID 00008110-… (25 chars), CoreDevice UUID E8A190DD-64F5-44A4-8D57-28E99E316D60 (36 chars).
  • pymobiledevice3 remote tunneld running stock-mode on 127.0.0.1:49151 inside havoc.
  • iosmux_inject.dylib (active, 150 KB, sha 26ca1658) loaded into every CoreDeviceService instance via an LC_LOAD_DYLIB entry the insert_dylib utility wrote into a patched copy of the binary at /Library/Developer/PrivateFrameworks/CoreDevice.framework/Versions/A/XPCServices/CoreDeviceService.xpc/Contents/MacOS/CoreDeviceService. The patched binary lives at the /Library/Developer/... framework root because the macOS build on this VM has no /System/Library/PrivateFrameworks/CoreDevice.framework (the Xcode-CLT install is the only CoreDevice framework on this machine, and was patched in place).
  • Pre-existing leftover peer records from earlier project iterations: four byte-distinct copies of E8A190DD-64F5-44A4-8D57-28E99E316D60.plist (NSKeyedArchiver CUPairedPeer) across /var/db/lockdown/RemotePairing/user_0/peers/, …/user_501/peers/, and the two container-sandbox roots for nullweft and root users of com.apple.CoreDevice.remotepairingd. Three of the four share content sha fe6bc97c… (timestamp shift only); the user_0 copy is sha 33b64dd0…. Same pk (Ed25519, prefix 0aebefad…), same altIRK (df641cca…), same ident UUID. Origin of these records is unconfirmed and they are not relied on for any of the iter-17/18 conclusions below.
  • Host-side identities are split: /var/db/lockdown/RemotePairing/user_501/identity.plist (sha 74e21111…, mtime Apr 10) has different pk/sk than the Containers/com.apple.CoreDevice.remotepairingd/.../identity.plist copies in nullweft and root sandboxes (both sha 2d262157…, mtime Apr 6). Same ident UUID c9877d69-… in all three; the pk/sk diverge. Container copy is the live one remotepairingd reads (per iter-16 fs_usage).

Run A — xcrun devicectl manage pair with active 150 KB inject

Command (executed as nullweft, not via sudo, so CoreDeviceService spawned in the user XPC domain):

xcrun devicectl manage pair --device E8A190DD-64F5-44A4-8D57-28E99E316D60

Result inside ~2 s:

ERROR: An error occurred while communicating with a remote process.
       (com.apple.dt.CoreDeviceError error 3 (0x03))
           The connection was interrupted. (com.apple.Mercury.error error 1000 (0x3E8))

iPhone-side Trust dialog: never displayed. devicectl did not get far enough to ask the iPhone for one.

CoreDeviceService log around the failure (compacted):

(iosmux_inject.dylib) [iosmux] OS_remote_device created: name=iPhone (iosmux) ...
(iosmux_inject.dylib) [iosmux] DYLD_INTERPOSE active: 62 services from Handshake, tunnel=[fd88:b337:ebe5::1]
(iosmux_inject.dylib) [iosmux] === PoC registration complete. Device visible in Xcode. ===
[com.apple.mobiledevice:All] Device '00008110-0004596E22A0401E' is no longer valid;
                              cannot start remote service 'com.apple.mobile.notification_proxy'.
[com.apple.mobiledevice:All] Failed to start service via RemoteServiceDiscovery: 0xe800000b (kAMDNotConnectedError)
[com.apple.dt.coredevice:rsddevicewrapper] Error fetching developer mode status from device:
    "Could not connect to the device."

Read: the inject does its PoC registration cleanly (the synthetic device is visible to Xcode); CDS then attempts RSD-level service calls keyed on the hardware UDID 00008110-0004596E22A0401E and finds that the device with that UDID has nothing it can connect to on the synthetic-device's RSD endpoint. The synthetic device is behind a tunnel address ([fd88:b337:ebe5::1]) that has no notification_proxy listener — the inject's PoC stops at "device exists in Xcode," it does not stand up the per-service handlers CDS expects post-pair.

This is consistent with the inject being a registration shim only. It is also the architecture this project has been operating under for weeks; iter-18's contribution is making that operational mode explicit.

Run B — same command with .disabled 96 KB stub swapped into the active path

Hypothesis tested: maybe the inject is the only thing blocking the real iPhone from reaching CDS through the normal RSD channel. If disabled, CDS would talk directly to tunneld → real iPhone, the native pair-setup ceremony would run, the user could tap Trust.

Swap performed (reversibly):

mv /Library/Developer/CoreDevice/iosmux_inject.dylib \
   /Library/Developer/CoreDevice/iosmux_inject.dylib.iter18-was-active
cp -p /Library/Developer/CoreDevice/iosmux_inject.dylib.disabled \
      /Library/Developer/CoreDevice/iosmux_inject.dylib
launchctl kickstart -k user/501/com.apple.CoreDevice.CoreDeviceService

(mv was used for the active → was-active step so dyld immediately sees one canonical file at the load path; cp -p for stub → active so the .disabled copy stays in place for repeatable roll-forward / roll-back. The patched CoreDeviceService binary has a hard LC_LOAD_DYLIB /Library/Developer/CoreDevice/iosmux_inject.dylib entry — see otool -L in the swap script — so a missing file would make CDS fail to launch entirely.)

Run B result:

$ xcrun devicectl list devices
No devices found.

$ xcrun devicectl manage pair --device E8A190DD-…
ERROR: ... (same Mercury 1000 shape as Run A)

Synthetic iPhone (iosmux) disappeared from devicectl list devices entirely. CoreDeviceService log shows a different failure mode:

[com.apple.network:connection] [C1 ... url: http://127.0.0.1:62078/device-info ...] start
[com.apple.network:connection] nw_socket_handle_socket_event [C1:2] Socket received CONNRESET event
[com.apple.network:connection] nw_socket_handle_socket_event [C1:2] Socket SO_ERROR [61: Connection refused]
... (same again on /services)
(iosmux_inject.dylib) [iosmux] FATAL: XPC proxy init failed

Two facts emerge:

  1. The 96 KB .disabled is not a no-op stub. It still installs itself into CDS, still tries to bring up its own XPC plumbing, and fails fatally during XPC proxy init. Symbol-table diff confirms it: nm reports 267 symbols (__text 0x7a10) for .disabled vs 411 symbols (__text 0xf13b) for the 150 KB active copy. The smaller variant lacks the full SPIKE / RSD hook surface but still carries the IosmuxMDProxy Objective-C class — that's why the synthetic device's name was earlier suffixed with (iosmux) even though pair was failing.
  2. CDS without functional inject hooks falls through to classic usbmuxd at 127.0.0.1:62078, and that fallback dies with Connection refused. usbmuxd itself is running on this VM (launchctl print system/com.apple.usbmuxd → state = running, pid 146) but its USB-attached device set is empty: USB passthrough into a libvirt VM is not supported per OSX-KVM upstream (see feedback_no_usb_passthrough.md), and even if it were, classic usbmuxd is dead at the protocol level for iOS 17+ (ADR-0001 and feedback_design_decisions.md). So 62078 has no listener on either side of the USB question.

The active inject's job is therefore not just "register a synthetic device" but also stand in for the missing usbmuxd: it intercepts classic-lockdown API calls CDS would have made through usbmuxd and serves them out of its own emulated state. Removing the inject removes both halves of the bridge — there is no fallback path to "just talk to the real iPhone."

Reverted swap immediately after measurement (active → 150 KB, stub → back to .disabled), kickstarted CDS again, confirmed iPhone (iosmux) reappears in devicectl list devices. VM is back in pre-iter18 stable state.

Backups taken

All host-side backups under ~/backups/iosmux/:

  • d17-pre-plant/ (3 files, 1612 B): real iPhone-related plists pulled before the iter-17 plant attempt.
  • d18-cleanup-pre/ (4 files, 2074 B): four sandboxed-container copies of the E8A190DD peer + identity plists, captured before the iter-18 cleanup of the planted E269C66F record.
  • d18-full-pre/iosmux-d18-stage-full/ (8 plists, plus inventory + sha256): full filesystem snapshot of every RemotePairing-relevant plist on havoc, taken at the start of iter-18 — both lockdown-side (/var/db/lockdown/...) and container-side (the two sandboxed copies under nullweft and root). All sha256 verified host-side against the staging sha256 file.
  • d18-inject-pre/ (4 dylibs, ~340 KB): all four iosmux dylib variants on the VM (iosmux_inject.dylib + .bak + .disabled + iosmux_swift.dylib) at iter-18 start. Pulled directly via scp havoc-root: to host; no /tmp staging.

Existing earlier backups still on disk (system-binaries-ios2641/, vm-inject-20260408/, inject-pre-rsd-conn/, remotepairingd-xpc-pre-insert/, RemotePairing.framework.full/, etc.) cover earlier slices; none of them holds a true pre-patch CoreDeviceService binary — system-binaries-ios2641/CoreDeviceService sha-matches the patched one byte-for-byte and strings confirms it contains /Library/Developer/CoreDevice/iosmux_inject.dylib already, so it is a post-patch capture mislabelled by date. No pre-patch original is currently available on the host.

Why this falsifies "transparent proxy" architecture

  1. Active inject path — synthetic device exists, inject intercepts enough to register it, but CDS's RSD calls go to a tunnel that has no real-device listeners on the per-service ports. Pair fails because the synthetic device cannot perform iOS Pair-Setup (it has no Pair-Setup endpoint).
  2. Stub inject path — synthetic device disappears, CDS falls back to classic usbmuxd at 62078, that fails because usbmuxd has no device. Pair cannot even start because there is nothing for manage pair to reach.

There is no third inject mode in the codebase that yields "real iPhone exposed to CDS as itself." Producing one would require re-introducing usbmuxd-as-pair-flow (refused at protocol level for iOS 17+) or wiring CDS directly to tunneld's pair-flow path (tunneld has no such path; pymobiledevice3 has its own non-CoreDevice pair surface that bypasses CDS). Both are out of scope.

The only remaining way for xcrun devicectl manage pair to succeed against a device routed through this VM is for the inject + iosmux backend to be the pair endpoint for the synthetic device, serving the lockdown-over-TCP three-verb API (RSDCheckin / QueryType / GetValue) and declaring the synthetic device's pairingState as paired without invoking iOS Pair-Setup against the real device. That is the Option δ direction (stage2.md "Option δ" section, connection.md "Phase S2.B note") and the work scoped by Phase D.6.6-impl.

Implications for next iter

  • iter-19+ does not attempt xcrun devicectl manage pair against the synthetic device again. The expected outcome is now known and documented; the experiment has been falsified.
  • iter-19+ does not pursue mDNS-advert planting / authTag derivation / Ed25519 challenge replay as a way to flip pairingState through remotepairingd. Same reason — those approaches presupposed the transparent-proxy architecture (already falsified by Phase S2.B, see s2b-pair-attempt-log.md) that iter-18 falsified.
  • Phase D.6.6-impl is the next concrete step: extend the cmd/iosmux-backend/ Go binary to bind [::1]:50367 (port already advertised in our Handshake Services dict under com.apple.mobile.lockdown.remote.trusted per iter-11), implement the three-verb classic-lockdown protocol, and serve a coherent device dictionary (likely seeded from the captured 88-key dict iter-15 obtained via pymobiledevice3 remote rsd-info against the real iPhone in a brief stock-tunneld window). Once that works, pairingState: paired should follow without any native iOS pair-setup ever running.
  • The iosmux backend's Go relay (/Users/nullweft/iosmux/iosmux-relay) was not running during iter-18. Whether running it would have changed Run A's kAMDNotConnectedError outcome was not measured; iter-19 can re-confirm the failure with relay running, but the expected result is still failure for the same architectural reason — relay terminating an RSD tunnel does not turn the synthetic device into a real-iOS Pair-Setup endpoint.

Evidence trail

  • Backups: ~/backups/iosmux/d18-full-pre/ (host) — every RemotePairing plist on havoc at iter-18 start, sha256-verified.
  • Backups: ~/backups/iosmux/d18-inject-pre/ (host) — all four iosmux dylib variants captured before swap.
  • Inject load mechanism: otool -L /Library/Developer/PrivateFrameworks/CoreDevice.framework/Versions/A/XPCServices/CoreDeviceService.xpc/Contents/MacOS/CoreDeviceService — last entry is /Library/Developer/CoreDevice/iosmux_inject.dylib (compatibility version 0.0.0, current version 0.0.0).
  • Run A log capture (CDS / remotepairingd / remoted / devicectl predicates, 4 min window): captured to /tmp/iosmux-d18-pair-log.out on havoc. Contains the kAMDNotConnectedError chain and the inject's PoC registration trace.
  • Run B log capture: same predicate set, replayed after the swap. Contains the Connection refused 127.0.0.1:62078 chain and the stub's FATAL: XPC proxy init failed line.
  • Symbol diff: nm /Library/Developer/CoreDevice/iosmux_inject.dylib.iter18-was-active | wc -l → 411; nm /Library/Developer/CoreDevice/iosmux_inject.dylib | wc -l → 267 (when stub was active).
  • usbmuxd state: launchctl print system/com.apple.usbmuxd → state = running, pid 146; no listener on 127.0.0.1:62078 per lsof -nP -iTCP:62078 (empty).
  • Host backup system-binaries-ios2641/CoreDeviceService sha256 1b4aa8a5f1e29078… matches the live patched binary on VM byte-for-byte; strings of the host backup contains /Library/Developer/CoreDevice/iosmux_inject.dylib — i.e. it is a post-patch capture, not a pre-patch original.

References

  • connection.md "Phase S2.B note" — already documents the bridge-not-proxy architecture that this iter empirically re-confirmed.
  • stage2.md "Phase B realization" and "Option δ" sections — same architectural framing, in the forward-plan voice.
  • s2b-pair-attempt-log.md — the original Phase S2.B finding (RST on tunnel) that established this.
  • iter-15-multifield-patch/findings.md — wire-level identity matching falsification that escalated H-PairState.
  • iter-16-pairstate-survey/findings.md — host-side localisation of the pair gate to RemotePairing peer store + Bonjour advert.
  • feedback_design_decisions.md (project memory, not in repo) — usbmuxd-as-pair REJECTED for iOS 17+. Cited for completeness; not linkable from public docs.
  • Apple MobileDevice.framework errno 0xe800000b (kAMDNotConnectedError) — observed in Run A log, signals the device cannot be reached for the requested service.