Skip to content

Plan: CDS Inject with Real RSD Transport

Date: 2026-04-13 Status: IMPLEMENTED. Device connected in Xcode. Action interception is the open work.

Summary

CoreDeviceService is injected via LC_LOAD_DYLIB (iosmux_inject.dylib). The dylib constructs a real xpc_remote_connection to iPhone via the pymobiledevice3 tunnel, parses the RSD Handshake (46 device properties + 74 services), creates the SDR and RSDDeviceWrapper, sets DeviceInfo fields, and registers the device with CDS. Result: device shows in devicectl and Xcode as state=connected.

Architecture

0.  iosmux_install_md_proxy()             — MobileDevice property proxy
1.  iosmux_rsd_init()                     — TCP connect + create xpc_remote_connection
                                            + activate + wait for Handshake (5s timeout)
                                            + parse Services dict into g_services[74]
2.  create_remote_device(conn)            — OS_remote_device alloc, set ivars,
                                            attach xpc_remote_connection at offset 80
3.  Resolve Swift symbols                 — DeviceInfo.init, SDR init, install(browser:),
                                            RSDDeviceWrapper.__allocating_init, etc.
4.  Build DeviceInfo                      — set name, productType, hwModel, marketingName,
                                            state=connected(3), pairingState=paired(2),
                                            visibilityClass=default,
                                            areDeveloperDiskImageServicesAvailable=true,
                                            preparednessState=.all (0xF)
5.  Build DeviceIdentifier                — 33-byte enum, UUID variant tag=0
6.  SDR.__allocating_init                 — passes empty capabilityImplementations dict
7.  Find ServiceDeviceManager via heap scan
8.  handleDiscoveredSDR(sdr)              — publishes device added event
8b. updateIdentifier()                    — registers SDR in managedServiceDevices
8c. (DYLD_INTERPOSE for remote_device_*   — already active at this point)
9.  connected_callback fire               — async polling
9b. RSDDeviceWrapper.__allocating_init    — completes in ~20ms, stores wrapper in
                                            g_hook_wrapper (Step 9b directly, not via
                                            sdr+104 read overflow)
10. Verify SDR in managedServiceDevices
11. Hook serviceDeviceRepresentations(forDeviceIdentifiedBy:) — returns our SDR
12. Hook CDS+0xCCB0 — uses g_hook_wrapper
13-14. (REMOVED — CoreDevice+0x30BEE0 / +0x30C33C shared cache patches)
15. Hook CDS+0x5E2D0 — callq patch returning wrapper for our UUID
17. (REMOVED — CoreDeviceUtilities DDI throw inline patch)
19. Hook CDS+0xB896 — currently `mov al, 1; ret`, broken (see Open Issues)

DYLD_INTERPOSE table (top of iosmux_inject.m)

Function Replacement Purpose
remote_device_copy_service_names g_interpose_service_names array Prevent blocking XPC query
remote_device_copy_service return NULL Prevent blocking XPC query
remote_device_copy_property return NULL Prevent blocking XPC query
remote_device_heartbeat callback(true) immediately Prevent blocking XPC heartbeat
remote_service_create_connected_socket TCP to tunnel + service port Service connection
remote_service_connect_socket TCP to tunnel + service port Service connection
xpc_remote_connection_create_with_remote_service create from connected fd RemoteXPC service connection

Linked with -F/System/Library/PrivateFrameworks -framework RemoteServiceDiscovery -framework RemoteXPC so the DYLD_INTERPOSE table has valid original pointers (otherwise dyld aborts at load).

Function Signature (proven)

xpc_remote_connection_t xpc_remote_connection_create_with_connected_fd(
    int fd,                    // TCP socket to RSD endpoint via tunnel
    dispatch_queue_t queue,    // target queue (concurrent — serial deadlocks)
    uint64_t version_flags,    // 0x100000000000006 (from RSD RemoteXPCVersionFlags)
    uint64_t mode_flags        // 0
);
// After activate(): receives Handshake event with MessageType="Handshake",
// MessagingProtocolVersion=7, Properties={46 device keys}, Services={74 entries}

Build and Deploy

ssh havoc "cd ~/iosmux/inject && make clean && make"
ssh havoc-root "cp /Users/nullweft/iosmux/inject/iosmux_inject.dylib /Library/Developer/CoreDevice/iosmux_inject.dylib"
ssh havoc-root "killall CoreDeviceService"
# Wait 3s for inject + RSD init, then check log:
ssh havoc "tail -50 /tmp/iosmux_inject.log"

Verified results

Item Status
RSD Handshake (46 properties + 74 services) OK
Real iPhone properties (iPhone14,6, iOS 26.4, SerialNumber, ...) OK
devicectl list devices shows state=connected OK
Device visible in Xcode Devices & Simulators OK
RSDDeviceWrapper created (init returns ~20ms) OK
pairingState=paired, areDDIServicesAvailable=true, preparednessState=.all OK
DYLD_INTERPOSE active for all 7 hooks OK
No shared cache __TEXT modifications OK

Open issues

CDS+0xB896 hook is broken

mov al, 1; ret returns true unconditionally for invoke(anyOf:usingContentsOf:). This short-circuits the Swift async continuation chain. Mercury never gets the result, never sends an XPC reply, Xcode times out with "error communicating with remote process". Click Pair → CDS dies (action handler crashes anyway because g_hook_wrapper was a garbage pointer from a separate read overflow bug, see below).

Correct fix per E+F research: convert CDS+0xB896 from mov al, 1; ret to a passthrough trampoline (movabs r10, <orig_invoke>; jmp r10), and intercept actions at a higher level instead.

Wrapper read overflow at sdr+104

Step 9b created wrapper as a __block void * local variable in if (rsd_wrapper_init && rsd_wrapper_meta). Step 12 then read g_hook_wrapper = *(void **)((uint8_t *)sdr + 104) — reading 8 bytes 16 bytes past the end of an 88-byte SDR object. The result was a garbage pointer from the adjacent heap object, returned to CDS by CDS+0xCCB0 and CDS+0x5E2D0 hooks.

Fixed in commit 59ca5cd: g_hook_wrapper = wrapper directly in Step 9b, removed the sdr+104 read in Step 12. Untested at the time of writing — may eliminate Pair crashes on its own.

Action interception strategy

Previous attempts assumed Xcode↔CDS uses the flat CoreDevice.featureIdentifier / CoreDevice.output dict format that pymobiledevice3 uses against the iPhone. This was wrong. Xcode↔CDS uses Mercury.XPCDictionary with Swift Codable + a mangledTypeName envelope. Filtering by string keys cannot work on that path.

See docs/research/action-interception-full-picture.md for the synthesis of three parallel research sessions on hook ABI, connection identification, and the actual Mercury wire format.

Rollback

# Quick: revert HEAD on inject/iosmux_inject.m and rebuild
# Backup: ~/backups/iosmux/inject-pre-interpose.dylib (last known-good before INTERPOSE)