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) |