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:
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.outputkey — 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/hasConnectionlogicdocs/research/coredevice-xpc-protocol.md— Mercury envelope descriptiondocs/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)