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). Partially superseded 2026-04-28 — see admonition below.
Partial supersession 2026-04-28 (session 14)
The "Critical correction" section below claims that
CoreDevice.deviceIdentifier, CoreDevice.input, and
CoreDevice.output "do not exist on the Xcode↔CDS connection"
based on a strings(1) search of CDS / CoreDevice /
DVTCoreDeviceCore returning zero hits. This claim was
empirically falsified on 2026-04-28 when a recv-side
Mercury XPC capture rig with a relaxed filter caught a real
Xcode→CDS Pair-action invocation containing literal
CoreDevice.deviceIdentifier, CoreDevice.input, and
CoreDevice.actionIdentifier keys at top level (full envelope
shape in
mercury-envelope-empirical.md
§"Pair action invocation envelope (session 14 Round 3,
2026-04-28)"). The strings search likely missed these because
the keys live inside the Mercury Swift overlay's static
string-table at offsets strings(1) does not surface, or
inside dyld_shared_cache regions that the search did not cover.
Treat the original claims about non-existence as wrong;
treat the recommendation that "filtering by a top-level string
key is impossible on this connection" as wrong; treat the
rest of this doc (Mercury Codable envelope description, Block
lifetime guidance, action-dispatch reasoning) as still useful
background, but cross-check any specific claim against the
empirical capture in mercury-envelope-empirical.md before
acting on it.
Mercury XPC carries TWO distinct envelope shapes on the same XPC connection:
- Codable event envelope —
{mangledTypeName, value}— used for events / requests / replies whose body is a single Swift Codable value. The doc below is correct for THIS envelope. - Action invocation envelope — flat dict with
CoreDevice.actionIdentifierdiscriminator, plusCoreDevice.deviceIdentifier,CoreDevice.invocationIdentifier,CoreDevice.input,CoreDevice.coreDeviceVersion,CoreDevice.CoreDeviceDDIProtocolVersionsiblings. Used for action invocations from Xcode to CDS. The doc below is NOT correct for this envelope — the "no top-level string discriminator" assumption is empirically wrong; the correct discriminator isCoreDevice.actionIdentifier.
Sub-task 3 (GAMBIT) action interceptor design therefore filters
on CoreDevice.actionIdentifier (envelope B), recognising
Apple action IDs like com.apple.coredevice.action.pair. The
Mercury Codable envelope filter is for events, not action
requests.
Critical correction (the key finding) — historical, see admonition above¶
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.
This evidence claim was falsified empirically 2026-04-28
Captured Mercury XPC payload (session 14 Round 3) shows
CoreDevice.deviceIdentifier AND CoreDevice.input AND
CoreDevice.actionIdentifier literally on the wire of an
Xcode→CDS Pair-action invocation. The strings search this
section relied on missed them. See the top-of-file admonition
and mercury-envelope-empirical.md for the live envelope.
Implication: Filtering by xpc_dictionary_get_string(event, "CoreDevice.featureIdentifier") in an xpc_connection_set_event_handler interpose cannot work for the Pair flow.
This implication is wrong as stated
Filtering by xpc_dictionary_get_string(event, "CoreDevice.actionIdentifier")
(the actually-used key, not featureIdentifier) DOES work for
the Pair flow — sub-task 3 GAMBIT relies on this. The
featureIdentifier key specifically is from the device-side
RemoteXPC protocol, not from Mercury; that part is correct.
The mistake was the broader generalisation that no top-level
string discriminator exists.
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.AcquireDeviceUsageAssertionActionDeclaration, 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.AcquireDeviceUsageAssertionActionDeclaration.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)