Skip to content

Action Interception — Full Picture from 3 Parallel Research Sessions

Date: 2026-04-13 Status: Research synthesis from three parallel research sessions (E+F, A+B, C+D).

Critical correction (the key finding)

A previous design assumed Xcode↔CDS uses the same CoreDevice.featureIdentifier / CoreDevice.output flat dict format that pymobiledevice3 uses against the iPhone. This is wrong. There are TWO different protocols:

Protocol Endpoints Format
Device-side RemoteXPC Mac CDS ↔ iPhone (over tunnel) Flat xpc_dict with CoreDevice.featureIdentifier / CoreDevice.input / CoreDevice.output
Local Mercury XPC Xcode ↔ CDS (on Mac) Mercury.XPCDictionary with Swift Codable + mangledTypeName envelope

Evidence: strings search of CoreDeviceService binary, CoreDevice framework, and DVTCoreDeviceCore for CoreDevice.featureIdentifier|deviceIdentifier|output returns zero hits. These keys don't exist on the Xcode↔CDS connection.

Implication: Filtering by xpc_dictionary_get_string(event, "CoreDevice.featureIdentifier") in an xpc_connection_set_event_handler interpose cannot work for the Pair flow.

Found bug: SDR read overflow at sdr+104

While reviewing code, found a critical bug from prior session cleanup:

// Step 12 (line 1164):
g_hook_wrapper = *(void **)((uint8_t *)sdr + 104);  // READ OVERFLOW

SDR is 88 bytes. We previously WROTE wrapper to sdr+104 (heap overflow, fixed in commit aee03c7) and read it back in Step 12. The write was removed but the read remained → reads garbage from adjacent heap object.

Fix: assign g_hook_wrapper = wrapper directly in Step 9b from the local wrapper variable, remove read in Step 12.

This bug may have caused (or contributed to) the crashes attributed to Pair action dispatch — g_hook_wrapper was a garbage pointer that hooks at CDS+0xCCB0 and CDS+0x5E2D0 returned to CDS, which then dereferenced it.

Status: FIXED. Test in isolation before any other changes.


Synthesized findings

From research E+F (hook interaction + edge cases)

Question Answer
Should CDS+0xB896 stay? Yes — but as passthrough trampoline, not return true. Current return true is the active bug causing "error communicating" by short-circuiting Swift async continuation.
xpc_dictionary_create_reply NULL? Means message has no reply context (fire-and-forget). Pass through to original.
Threading for send_message? Safe from event handler thread, no dispatch_async needed.
Echo invocationIdentifier in reply? No. libxpc reply-context matching is enough.
Idempotency? Stateless wrapper. New UUID per acquireusageassertion call.
Connection error events? Always forward XPC_TYPE_ERROR to original handler.
Block ownership? libxpc Block_copies handler. We don't release captured handler parameter.
Reject action format? Reply with empty dict and NO CoreDevice.output key (forces Codable decode failure → clean error).

From research A+B (DYLD_INTERPOSE + connection identification)

Question Answer
Call original from interpose? Direct call by name. DYLD_INTERPOSE only rewrites GOT in OTHER images. Same-image calls bind to real symbol.
Wrap xpc_handler_t block? Block_copy(handler) → capture in ^{} block → libxpc Block_copies wrapper itself. No autorelease concerns.
Multiple set_event_handler calls? Replaces previous (only legal pre-resume). No hash table needed — each wrapper closes over its own captured original.
xpc_connection_get_name(peer) for CDS? Returns "com.apple.CoreDevice.CoreDeviceService" (listener name). Clean filter.
Timing of set_event_handler? After our constructor (LC_LOAD_DYLIB loads before main()). We intercept all calls.
Filter non-CDS connections? Whitelist by name + secondary check inside wrapper. Direction-agnostic safety.

From research C+D (XPC message format) — THE BOMBSHELL

Question Answer
Xcode→CDS message format? Mercury.XPCDictionary with Swift Codable + mangledTypeName envelope. NOT flat keys.
CoreDevice.featureIdentifier exists? NO. Action type identified by Swift mangled type name in mangledTypeName envelope key.
Action types as Swift symbols? CoreDevice.PairActionDeclaration, CoreDevice.AcquireBUsageAssertionActionDeclaration, CoreDevice.ConnectActionDeclaration, CoreDevice.EnableDeveloperDiskImageServicesActionDeclaration, etc.
Device identifier format? CoreDevice.DeviceIdentifier Swift enum (Codable). Cases: .ecid(UInt64), .uuid(UUID, String). NOT raw xpc_uuid.
acquireusageassertion Output? Type confirmed: CoreDevice.AcquireBUsageAssertionActionDeclaration.Output. Internal fields UNKNOWN.
pair Output format? Type: CoreDevice.PairActionDeclaration.Output. Has nested ChallengeAnswer — interactive multi-message protocol, not single req/rep!
enableddiservices Output? Has nested ProgressUnit — streams progress. Not one-shot.
Sequence of actions on Pair click? UNKNOWN from static analysis. Needs runtime trace.

Strategy for forward path

Phase 0: Test wrapper fix in isolation (NEXT STEP)

Deploy ONLY the read-overflow fix at sdr+104. Test: 1. Device still connects in Xcode 2. Click Pair — observe behavior 3. May reveal that prior crashes were due to garbage g_hook_wrapper, not action dispatch issues

If Pair stops crashing → proceed normally to action interception If Pair still crashes → continue to Phase 1

Phase 1: Capture real Mercury XPC messages (if Phase 0 doesn't fix it)

Since we don't know Mercury's wire format, we can't write a proper interceptor. Need to observe real messages first.

Approach: install a logging-only interpose on xpc_connection_send_message and xpc_connection_send_message_with_reply in CDS process. For any message on com.apple.CoreDevice.CoreDeviceService connection: 1. xpc_copy_description(message) → write to log file 2. Pass through to original (no behavior change) 3. Click Pair in Xcode → capture full Mercury exchange 4. Analyze captured format to understand mangledTypeName envelope structure

Phase 2: Design proper interceptor based on captured data

Once we know exact Mercury format, choose between:

Option A: XPC-level Mercury parser Parse mangledTypeName from incoming dict, recognize known action types, build properly-formatted Codable response. Hard because Mercury Codable wrapping is undocumented.

Option B: Swift-level invoke() hook Hook ActionImplementation.invoke() per-action-type. Requires Swift async ABI work but action is already decoded into typed object at this level. May be more tractable than C-level Mercury parsing.

Option C: Hybrid - XPC-level: filter by Swift mangled type name (string check on mangledTypeName value) - Build response by copying a known-good response from another action (steal Codable bytes) - Substitute UUID/identifiers as needed

Phase 3: Pair-specific challenge/answer protocol

PairActionDeclaration is NOT a single req/rep. It has ChallengeAnswer sub-protocol — likely: 1. Xcode → CDS: PairActionDeclaration.Input (with challenge) 2. CDS → Xcode: ChallengeAnswer (via reply) 3. Xcode → CDS: more challenges 4. ... eventually Output

Simple "return empty Output" won't satisfy the protocol. Need to either: - Implement minimal challenge/answer flow - Convince Xcode that pairing already happened so it skips PairAction entirely - Set DeviceInfo state such that Xcode doesn't trigger PairAction in the first place

Critical insight: pair button cause

hasConnection = (_shadowUseAssertion != nil && state == .fulfilled)

_shadowUseAssertion is set by Xcode's local code when RemoteDevice.acquireDeviceUsageAssertion returns success. This is a DVTCoreDeviceCore client-side call that goes through Mercury to CDS. We need to make CDS respond successfully to this — without it, hasConnection=false → Pair button always shown.

This is the SINGLE most important action to handle correctly. If we get this right, Xcode shows the device as connected/ready and may not need to send PairAction at all.


Conclusions

Wrong assumptions to avoid

  • Filter by xpc_dictionary_get_string(event, "CoreDevice.featureIdentifier") — key doesn't exist on Xcode↔CDS
  • Build reply with CoreDevice.output key — wrong envelope
  • Return {"assertionIdentifier": <uuid>} as a flat dict — wrong format (Codable, not flat dict)

The flat-dict format IS correct for the device-side RemoteXPC protocol that pymobiledevice3 uses against the iPhone. It is wrong for the Xcode↔CDS local Mercury XPC.

CDS+0xB896 disposition

E+F research clarified: change from mov al, 1; ret to passthrough trampoline (movabs r10, <orig>; jmp r10). The current return-true is the active bug, not a fix. With passthrough:

  • If a higher-level interceptor handles the message: invoke() never called, trampoline silent
  • If the higher-level interceptor misses: invoke() called normally, may crash but observable (not masked by a timeout)

Don't write the interceptor yet

Until we have captured Mercury messages and understand the Codable envelope structure, any interceptor code would be speculation. Phase 0 (test wrapper fix) and Phase 1 (logging interpose to capture real Mercury exchange) are the only safe next steps.


File references

  • inject/iosmux_inject.m — wrapper fix in Step 9b and Step 12 (commit 59ca5cd)
  • docs/research/pair-button-and-cfnetwork.md_shadowUseAssertion / hasConnection logic
  • docs/research/coredevice-xpc-protocol.md — Mercury envelope description
  • docs/research/rsd-wrapper-init-analysis.md — RSDDeviceWrapper.init call chain
  • pymobiledevice3 source: pymobiledevice3/remote/core_device/core_device_service.py — device-side protocol reference (NOT applicable to Xcode↔CDS)