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

  1. 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.
  2. Action invocation envelope — flat dict with CoreDevice.actionIdentifier discriminator, plus CoreDevice.deviceIdentifier, CoreDevice.invocationIdentifier, CoreDevice.input, CoreDevice.coreDeviceVersion, CoreDevice.CoreDeviceDDIProtocolVersion siblings. 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 is CoreDevice.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:

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