Skip to content

RSD DeviceWrapper Init — Complete Analysis

Date: 2026-04-12 Status: RESOLVED — RSDDeviceWrapper.init works after heartbeat hook + crash fix

Summary

RSDDeviceWrapper.init was disabled because it "hangs". Analysis of 4 research sessions found that we never tested init with all hooks enabled. The hooks for remote_device_copy_service and remote_device_copy_property were added in the SAME commit that disabled init. With all hooks, init should complete in ~20ms.

Complete Call Chain

RSDDeviceWrapper.__allocating_init(remoteDevice:, deviceIdentifier:)
├─ remote_device_copy_service_names(device)     ← HOOKED → static array (10 names)
├─ [possible] remote_device_copy_service(...)   ← HOOKED → NULL
├─ MobileDeviceRef.init(remoteDevice:)
│   └─ _AMDeviceCreateWithRemoteDeviceWithError(device)
│       ├─ remote_device_copy_product_type(device)  ← NOT hooked, reads _properties ivar
│       ├─ remote_device_get_type(device)            ← NOT hooked, reads _type ivar (=14)
│       ├─ remote_device_get_name(device)            ← NOT hooked, reads device_name ivar
│       ├─ dispatch_semaphore_create(0)
│       ├─ remote_device_set_connected_callback(device, queue, block)
│       │   └─ dispatch_sync(device._dq, ^{
│       │       [device state] → reads _state=2 → dispatch_async(queue, callback)
│       │   })
│       ├─ dispatch_semaphore_wait(sem, 5s timeout)
│       │   └─ callback fires via dispatch_async → signal → returns IMMEDIATELY
│       │
│       │   [macOS 14+ path — heartbeat SKIPPED entirely]
│       │
│       ├─ remote_device_copy_uuid(device)           ← NOT hooked, reads _uuid ivar
│       └─ return AMDeviceRef (success)
├─ remote_device_copy_property("DeviceSupportsLockdown")  ← HOOKED → NULL → fallback OK
├─ remote_device_copy_property("ReleaseType")             ← HOOKED → NULL → fallback OK
├─ remote_device_copy_property("IsUIBuild")               ← HOOKED → NULL → fallback OK
├─ remote_device_copy_service("installcoordination_proxy") ← HOOKED → NULL → fallback OK
├─ remote_device_set_disconnected_callback(...)            ← NOT hooked, stores callback
└─ retq → wrapper created

Why Previous Tests Hung

Test Hooks active Result Explanation
Test 1 copy_service_names only HUNG copy_service and copy_property NOT hooked → dispatch_sync XPC queries blocked
Test 2 all three + init disabled N/A Never tested — hooks added and init disabled simultaneously
Test 3 (next) all three + init enabled EXPECTED: ~20ms All blocking functions hooked

Functions NOT Hooked (safe — verified by disassembly)

All these functions use dispatch_sync(device._dq, block) internally. With our concurrent queue, dispatch_sync executes block inline on current thread (no blocking). None of them send XPC messages — they read from ObjC properties / ivars.

Function Mechanism (disassembled) Our value
remote_device_copy_product_type dispatch_sync → [device properties] → xpc_dictionary_get_string("ProductType") → strdup "iPhone14,6"
remote_device_get_type reads _type (offset 32) directly 14 (network-peer)
remote_device_get_name reads device_name (offset 8) directly "iPhone (iosmux)"
remote_device_copy_uuid dispatch_sync → [device uuid] ObjC getter → uuid_copy to output buffer E8A190DD-...
remote_device_set_connected_callback dispatch_sync → [device state] → if ==2: dispatch_async(callback) state=2 → immediate
remote_device_set_disconnected_callback stores callback at offset 112

All UNKNOWN items RESOLVED. remote_device_copy_uuid confirmed: dispatch_sync + ObjC getter, NO XPC calls. With concurrent device queue, all dispatch_sync calls are instant.

Connected Callback Mechanism (verified by disassembly)

remote_device_set_connected_callback does dispatch_sync(device._dq, block). Inside block: [device state] reads offset 28 (0x1c) via ObjC getter. If state == 2 or state == 3 → dispatch_async(callbackQueue, callback). callback signals the semaphore.

Our device._state = 2, device._dq = concurrent queue → NO deadlock possible. dispatch_async delivers callback in ~0-10ms → semaphore signaled immediately.

Heartbeat — CORRECTION: NOT Dead Code (previous research was WRONG)

Previous research claimed heartbeat is skipped on macOS 14+. This was WRONG. Verified by test: remote_device_heartbeat IS called on macOS 26.

The __isPlatformVersionAtLeast(macOS 14) check likely gates a DIFFERENT code path, not heartbeat itself. Heartbeat is called AFTER connected callback succeeds.

remote_device_heartbeat(device, queue, callback): - Does dispatch_sync(device._dq, ^{ send {"cmd":"heartbeat"} via _connection }) - Waits for XPC reply with {"result": "OK"} - Our xpc_remote_connection is server-mode HTTP/2 → cannot send request/reply - dispatch_sync blocks forever inside the block (XPC send never completes) - _AMDeviceCreate never returns

This is the ROOT CAUSE of the init hang. Solution: hook remote_device_heartbeat to call callback(true) immediately.

remote_device_heartbeat — verified signature and hook design

void remote_device_heartbeat(
    OS_remote_device *device,    // %rdi
    dispatch_queue_t queue,      // %rsi — callback queue
    void (^callback)(bool)       // %rdx — ObjC block: ^(bool success)
);

Internal flow: 1. Creates XPC dict: {"cmd": "heartbeat"} 2. Gets [device connection] (ObjC getter) 3. Calls xpc_connection_send_message_with_reply(connection, dict, queue, internal_block) 4. Internal block checks reply for {"result": "OK"} → calls callback(success)

Hook: call callback(true) immediately via block invoke at offset 0x10. Block calling convention: ((void(*)(void*,bool))block[2])(block, true) — %rdi=block, %esi=1

Only caller: _AMDeviceCreateWithRemoteDeviceWithError. Global hook safe. After heartbeat: all remaining steps are local (copy_uuid, create AMDeviceRef). No more blockers.

Timeout Values (from disassembly)

Device type Timeout
iFPGA 120 seconds
type 10 (coredevice-device) 10 seconds
default (including type 14) 5 seconds

XPC Remote Connection — Server Mode (why request/reply fails)

xpc_remote_connection_create_with_connected_fd operates in HTTP/2 server mode. It accepts client preface from iPhone and mirrors streams. Handshake arrives because iPhone (remoted) initiates as client.

For request/reply: macOS side must open client-initiated streams (ROOT_CHANNEL stream 1, REPLY_CHANNEL stream 3). Server-mode connection does NOT do this automatically.

pymobiledevice3 comparison: - Opens ROOT_CHANNEL (stream 1) and REPLY_CHANNEL (stream 3) explicitly - Sends requests on stream 1, receives replies on stream 3 - xpc_remote_connection does NOT open these streams → request/reply impossible

This means: functions that send XPC messages through _connection WILL block forever. But with all blocking functions hooked → this is not a problem for init.

Tunnel Architecture

Port 51308 is NOT a pymobiledevice3 proxy — it's a TUN device route. TCP connections go directly through the encrypted tunnel to iPhone's remoted. The tunnel is L3 (IPv6 routing), not TCP proxy. Data is not intercepted by Python.

Handshake Content

The RSD Handshake contains: - MessageType: "Handshake" - MessagingProtocolVersion: 7 - UUID: device session UUID - Properties: 46 device keys (ProductType, SerialNumber, UDID, OSVersion, etc.) - Services: 74 service entries (ports, properties, UsesRemoteXPC flags)

Services dict eliminates need for Go relay API for service discovery.

State Machine: connecting → connected

Normal flow: 1. RPDR state → "available" (needs RSDDeviceWrapper + TunnelUsageAssertion + ConnectableDevice) 2. SDR.mutateState → stateChangedHandler → RemoteDevice.updateState(markServiceConnected: true) 3. Client sees state=connected

Key dispatch thunks: - SDR.mutateState: CoreDevice+0x28FB90 - SDR.updateState: CoreDevice+0x28FB10 - RemoteDevice.updateState: CoreDevice+0x183F70

markServiceConnected defaults to false (xor eax,eax). Set to true only from ConnectActionImplementation.

Step 9 Connected Callback Polling — Unnecessary

Step 9 polls offset 88 for connected_callback. But when _state==2, remote_device_set_connected_callback fires callback via dispatch_async WITHOUT storing it at offset 88. The polling loop runs 300 iterations (3s) and finds nothing. This is harmless but useless.

OS_remote_device Ivar Layout (verified)

Offset Ivar Type Our value
8 device_name char* "iPhone (iosmux)"
16 device_alias char* NULL
24 _remotexpc_tls_enabled BOOL 0
28 _state uint32_t 2 (connected)
32 _type uint32_t 14 (network-peer)
40 _dq dispatch_queue_t concurrent queue
48 _properties xpc_object_t Handshake Properties dict
56 _uuid uuid_t (16 bytes) E8A190DD-...
64 _device_id uint64_t 0
72 _messaging_protocol_version uint64_t 0x100000000000006
80 _connection xpc_object_t OS_xpc_remote_connection
88 _connected_callback block (set by MobileDevice)
96 _connected_callback_queue dispatch_queue_t (set by MobileDevice)
104 _connected_callback_self_retain id (set by MobileDevice)
112 _disconnected_callback block (set by init)
120 _disconnected_callback_queue dispatch_queue_t (set by init)
128 _disconnected_callback_self_retain id (set by init)