AcquireDeviceUsageAssertion side-channel mechanism — current state (post-D46d)¶
Status: verified — 2026-05-03, Direction F target identified + D46d resolved metadata-accessor offset; D46e c-developer dispatch ready
Compaction-survivable architectural anchor for Q-D66-15. Reflects
the cumulative empirical chain D21..D46c-fix. After 8 falsifications
closed alternative axes, D46 traced the errorCode 3 firing site
to FUN_100020700 and the gate predicate to
_get_effectiveDeviceIdentifier. D46b confirmed thunk symbol
+ 15-byte clean prologue + non-cdecl calling convention. D46c
(c-developer impl) authored the hook code (clean build, clean
deploy, CDS-stable) but BLOCKED on resolving the
_$s10CoreDevice16DeviceIdentifierOMa type metadata accessor —
not in CoreDevice's exports trie. D46c-fix added a Mach-O
LC_SYMTAB walker as fallback — empirically found per-image
symbol table ALSO blank for the target. Tahoe 26.x dyld shared
cache strips local symbols; both dlsym and nlist walks miss
internal Swift symbols. Hook code is dormant + ready, awaiting
metadata accessor address via path #2 (Ghidra-derived hardcoded
offset) OR path #3 (dynamic metadata recovery via Swift runtime
on first hook entry). Per-iteration detail lives in history
archives:
aua-history-d24-d35.md— D24 common funnel + D30 AFM peer keep-alive failed + D31 peer[1013] race + D32 kernel-driven invalidation + D33 devicectl-injection feasibility + D34/D35a Heisenbug.aua-history-d36-d37.md— D36 host-side static disasm + D37-C continuation ABI + D37-D listener-deinit hook target + D37-E dtrace falsification + D37-F cross-binary trigger search + G-4 alternative + D37-G CDS-side counter-instrumentation.aua-history-d38-d40.md— D38 G-4 / G-1 / G-1' devicectl-side cause-side hook falsifications + D39 Xcode-side H#2 deploy + Xcode integrity incident + recovery + D40 state-aware retrospective + D41 G-2 peer-wrapper falsification + D42 Ghidra C-probe (architectural inversion).
All findings to date were recorded in apparatus state S3 (Xcode running, Devices and Simulators open, Pair clicked) per the state-tagged probe template. D40 P1 retest empirically falsified "state determines AUA outcome" (cumulative 66/66 Path-B deterministic across S1+S2+S3 cells × naked + every wrapper variant tested); state capture remains discipline, not an outcome predictor in the current configuration.
TL;DR — two-channel handshake with reversed directionality¶
AcquireDeviceUsageAssertion is NOT a single request/reply over
Mercury XPC. It is a two-channel handshake:
- Action channel — the inject-synthesised
success(...)reply that GAMBIT already produces (Phase 1 baseline). The action reply lands and is accepted. - Side channel — a SECOND XPC peer connection. Per D42 Q2 the
directionality is the OPPOSITE of what D21..D40 assumed: the
anonymous listener half lives in the CD/devicectl process; CDS
is the CLIENT. CDS receives the listener endpoint INBOUND in
CoreDevice.input.endpointof the AUA action invocation and constructs its peer half viaxpc_connection_create_from_endpoint.
The action-channel reply alone is necessary but not sufficient. The
gate that produces errorCode 3 ("Failed to acquire assertion") is
on the side-channel — but per D42 the gate is payload structural
fidelity, not connection lifetime (5 cause-side falsifications
proved lifetime suppression is empirically infeasible — see below).
Architectural inversion (D42 Q2)¶
[devicectl/CD process] [CDS daemon process]
anonymous listener half ◄──── side-channel peer connection
(xpc_endpoint_create at CD+0x30afd) (xpc_connection_create_from_endpoint)
handler block: CD+0x2fb30 wrapped in Mercury.{System|Remote}XPCPeerConnection
├ event-handler installed via Mercury Swift API
└ sends AssertionFulfilledMessage outbound
Three CFE call sites for xpc_connection_create_from_endpoint exist
in CDS at 0x100020481, 0x10002120d, 0x10005e461 (the last is
FetchDyldSharedCacheFiles, out of AUA scope). Anonymous-listener
provider in Mercury is Mercury.XPCSideChannel.anonymousListener()
— CD/devicectl reaches this; CDS never references it.
MACH_NOTIFY_NO_SENDERS fires on the listener side (in
CD/devicectl) when the last peer-side send-right (in CDS) is
dropped. The send-right holder is unambiguously inside CDS's
Mercury wrapper instance. This inverts the cause-side fix
direction that D21..D40 was searching for in CD/devicectl: the
holder is in CDS itself.
Wire format — AssertionFulfilledMessage (D42 Q1)¶
CoreDevice.DeviceUsageAssertion is a CLIENT-SIDE Swift CLASS
(Hashable + Equatable, NO Codable conformance). Stored properties:
4 fields (_lockedMutableState, identifier: UUID,
options: UsageAssertionOptions, reason: String) + computed
state: State { case fulfilled / case invalidated(Error?) } that
dispatches via class-method-table to read _lockedMutableState.
DUA is not on the wire. The wire-format Codable that flows on
the side-channel peer is CoreDevice.AssertionFulfilledMessage:
struct CoreDevice.AssertionFulfilledMessage : Codable {
var identifier: Foundation.UUID
var updatedSnapshot: <SomeStateSnapshot> // !!! gap — TMA at CD+0x29bb30
}
The DUA wrapper is built CLIENT-SIDE in CD's listener-handler block
at CD+0x2fb30 by decoding an inbound AFM. Construction site at
CD+0x2ff02 calls swift_allocObject with metadata from the
type-metadata accessor at CD+0xd3570, then writes 4 stored
properties to runtime-patched offset globals at
0x41e8b0..0x41e8c8 and 0x3e7d80.
CD's listener handler block at CD+0x2fb30 decodes the wire enum:
- payload 1 → AFM-success → materialize DUA →
Result.success(DUA)to awaiter completion. - payload 2 → AFM-error →
Result.failure(error)to awaiter. -
1 message → warning "Received > 1 assertion fulfillment for anonymous assertion listener".
- "Peer is unexpectedly nil" → if peer connection dropped before AFM arrives → error path.
GAMBIT already emits AFM today via gambit_emit_afm_via_endpoint
in inject/iosmux_gambit.m. AFM is the right vehicle. Whether
the AFM payload currently matches CD's decoder is the open
question — see Forward direction below.
Side-channel peer release is KERNEL-driven (D32)¶
Peer connection invalidation that surfaces to the AUA awaiter as
XPCError(errorCode: 1001, "The connection was invalidated.") is
fired by MACH_NOTIFY_NO_SENDERS delivered by the kernel when
the OTHER side drops its last mach send right. The cancel chain is
entirely inside libxpc.dylib:
Frame Function Module / offset
#00 _xpc_connection_cancel libxpc + 0x4d2f7
#01 do_mach_notify_no_senders +0x3c libxpc + 0x407c0
#02 _Xmach_notify_no_senders +0x21 libxpc + 0x40761
#03 notify_server +0x4e libxpc + 0x3ff32
#04 _xpc_connection_pass2mig +0x8e libxpc + 0x3fe77
#05 _xpc_connection_mach_event +0x4d5 libxpc + 0x39755
#06+ _dispatch_client_callout4 / dispatch / pthread bottom
ZERO application frames in the cancel chain — no CD/CDU/Mercury/CDS
function appears. The release is a passive libxpc Mach-event reaction
to MACH_NOTIFY_NO_SENDERS. There is no app-code call site to hook
to "prevent the cancel". This empirically falsifies the D21..D29
working model (NOP removeCachedXPCConnection at CD+0xdbe0) as a
complete fix.
D42 Q3 separately enumerated CDS application-code XPC API surface and found zero cancel/release callers on the AUA path. CDS-side lifecycle is 100% Swift-ARC mediated — there is nothing to hook in app code on either side.
Cumulative falsifications (8 independent attempts; both axes exhausted)¶
Cause-side axis (lifecycle suppression) — 5 falsifications¶
| # | Probe | Hook target | Verdict |
|---|---|---|---|
| 1 | D23 P-1a | ActionConnectionCache.removeCachedXPCConnection (CD+0xdbe0) |
PARTIAL — eliminated XPCError 1001 on action-XPC eviction path; residual errorCode 3 still fires from a separate input. Hook still deployed today (D23 v2 baseline). |
| 2 | D30 | AFM peer keep-alive (g_afm_retained_peers global retain) |
FAILED — peer lifetime is not the gate. |
| 3 | D38 G-4 | Extra retain on AUA-completion context at CD+0x30791 (lVar17+0x18) |
FAILED — lVar17+0x18 not a strong reference. |
| 4 | D38 G-1 / G-1' | Mercury listener wrapper deinit body / __deallocating_deinit (Mercury+0x4e930 / +0x4e960) |
FAILED — listener Swift wrappers do not hold the send right. |
| 5 | D41 G-2 | Mercury peer wrapper __deallocating_deinit (Mercury+0x4ea10) |
FAILED — peer Swift wrappers do not either. All Mercury Swift wrappers exhausted. |
Plus D42 Q3: zero CDS-side application-code cancel/release callers. Lifecycle 100% Swift-ARC mediated. Cause-side suppression of the kernel cascade is empirically infeasible from any process within reach of the existing inject mechanism.
Payload-fidelity axis (Codable type mismatch) — 2 falsifications¶
| # | Probe | Question | Verdict |
|---|---|---|---|
| 6 | D43 | Does TMA at CD+0x29bb30 resolve to a type DIFFERENT from DeviceStateSnapshot (the type GAMBIT puts in AFM updatedSnapshot)? |
FALSIFIED. TMA resolves to CoreDevice.DeviceStateSnapshot byte-for-byte. Type identity matches. CodingKeys match exactly: 3 keys (deviceInfo, capabilityImplementations, monotonicIdentifier) — same as GAMBIT emits. GAMBIT also emits a spurious state key, but auto-synth KeyedDecodingContainer<CodingKeys> ignores keys not in CodingKeys, so it is decoder-tolerated noise. |
| 7 | D44a + D44b' | Does Apple's auto-synth Codable decoder reject the empty xpc_array GAMBIT currently puts in capabilityImplementations? |
FALSIFIED. D44a determined target shape = xpc_array-of-pairs (Capability {name, featureIdentifier} + [String]). D44b' deployed an unconditional non-empty entry (one Capability pair). Inject log confirmed new path executed. Unified log: ZERO Codable / typeMismatch / keyNotFound / dataCorrupted / Capability decoder errors anywhere. Apple decoder reported success() ~13 ms BEFORE the errorCode 3 fired on the useassertion subsystem — the failure is post-decode, NOT a Codable shape issue. |
Identifier-mismatch axis (D30 hypothesis #1) — 1 falsification¶
| # | Probe | Question | Verdict |
|---|---|---|---|
| 8 | D45 | Does Apple's AUA wrapper generate and track a per-assertion-instance UUID separate from CoreDevice.deviceIdentifier that GAMBIT (echoing deviceIdentifier into AFM identifier) cannot know about? |
FALSIFIED. AUA wrapper async body (CD+0x317a0) and completion-handler body (CD+0x304a0) call neither uuid_generate_random nor CFUUIDCreate nor Foundation.UUID.init() — no per-call UUID minted client-side before the action goes on the wire. CD-side AFM-decode handler at CD+0x2fb30..0x2ff3d copies AFM.identifier byte-for-byte into DUA.identifier via UUID value-witness initializeWithCopy with NO equality compare branch anywhere. The DUA's identifier is purely a mirror of whatever the AFM payload carried; GAMBIT's current echo of deviceIdentifier is correct. AUA action invocation envelope has only {reason, options, endpoint} in Input — no per-assertion UUID is sent on the wire either. CoreDevice.IdentifiableAssertionDetails (CD+0x290330) exists but is on the parallel TunnelAssertionRequest.identifiable tunnel-management API surface, not on the AUA endpoint side-channel path. |
Plus D44b apparatus learning: env-gate via launchctl setenv does
NOT propagate to CoreDeviceService XPC service post-killall respawn
on Tahoe 26.x (SIP off). Cause unknown; symptom empirical (verified
via launchctl procinfo on respawned CDS — IOSMUX_* absent from env
vector). Use file-presence gate or unconditional patch for any future
CDS-side runtime gate. Captured in memory rule
feedback_no_env_gate_for_cds_xpc.
Forward direction — Direction F CONFIRMED + bridgeable post-D46¶
D46 (2026-05-03 Ghidra static probe) empirically traced the
[useassertion] Failed to acquire usage assertion ... errorCode 3
emission to its CDS-internal firing site, identified the gate
predicate, and produced a viable D46b implementation roadmap.
D44b' empirical timeline (load-bearing 13ms window)¶
D44b' unified-log timeline (lines 22-29 of
/tmp/iosmux-d44b-prime-unified-log.txt at probe time):
22 [action] Invoking AcquireDeviceUsageAssertion ... invocation=171380B5...
23 [action] forward(...) Forwarding to ExecutionLocation.coreDeviceService
24 [action] Forwarding to ExecutionLocation.elsewhere(<SystemXPCPeerConnection ... pid=2222 ...>)
25 [action] Received reply from forwarded action: SUCCESS()
26 [action] Received reply from action: SUCCESS()
27 [analytics] Core Analytics ...
28 [analytics] Reporting to CA with Name: com.apple.CoreDevice.Action.Analytics
29 [useassertion] Failed to acquire usage assertion ... CoreDeviceError(errorCode: 3, ...)
D46 finding: the success() at line 25 is the synchronous routing
reply from the forward(...) infrastructure (NOT the action's actual
outcome). The 13 ms gap to line 29 is the time FUN_100020700 (AUA
action body) spends doing type-metadata-accessor instantiation,
SDR lookup, gate predicate evaluation, and error construction +
continuation throw.
D45 Q4 confirmed — emission is CoreDeviceService-side¶
grep -ac useassertion enumeration (D45):
| Binary | hits |
|---|---|
| CoreDevice | 0 |
| CoreDeviceUtilities | 0 |
| Mercury | 0 |
| devicectl | 0 |
| CoreDeviceService | >0 (subsystem string com.apple.dt.coredevice.useassertion. at slice VA 0x1000b7d90) |
D46 confirmed this empirically by locating the firing function inside the CDS binary.
D46 finding — firing function + predicate¶
Firing function: FUN_100020700 at slice VA 0x100020700 in
CDS (size 0x11fc bytes). This is the AUA action run/execute
body, called from the action dispatcher FUN_100018ea0 at
0x100018ea5 (UNCONDITIONAL_CALL).
Gate predicate (decompiled from FUN_100020700, citation:
/tmp/iosmux-d46-q2-funcsearch.txt:3266-3354):
// 1. extract UUID from inbound action invocation
CoreDeviceUtilities::Context::get_deviceIdentifier(...);
// 2. look up per-device ServiceDeviceRepresentation by UUID
CoreDevice::CoreDeviceService::_serviceDeviceRepresentation(<UUID>, conformingTo);
// 3. GATE — read Optional<DeviceIdentifier>
CoreDevice::ServiceDeviceRepresentation::_get_effectiveDeviceIdentifier(this, out);
// 4. enum-witness "isCase(.none)"
iVar14 = (*opt_witness_isCase_none)(...);
if (iVar14 == 1) { // .none → FAILURE PATH
CoreDevice::CoreDeviceError::get_deviceRepresentationMissingIdentity(...);
Swift::Error::_init(...);
_swift_allocError(...);
CoreDeviceUtilities::_Continuation::_resume(error); // throws errorCode 3
_swift_errorRelease(...);
return; // <-- this is the errorCode 3 emitted ~13ms after success() reply
}
// gate passed → extract Input.{reason, options, endpoint} → dispatch to FUN_10001fa00
Predicate semantics in plain English:
let effectiveDeviceID: Optional<DeviceIdentifier> =
serviceDeviceRepresentation._effectiveDeviceIdentifier
if effectiveDeviceID == nil {
throw CoreDeviceError.deviceRepresentationMissingIdentity
}
Cited code-site addresses (slice VAs in CDS):
0x100020700—FUN_100020700AUA actionrunbody (firing function).0x100020cff— predicate call site_get_effectiveDeviceIdentifier(this, out).0x100020d5d— error constructor_get_deviceRepresentationMissingIdentity(...).0x100020df3—_Continuation::_resume(error)(the throw point that surfaces errorCode 3 to devicectl).0x100018ea0/100018ea5— action router → AUA-body dispatch.0x10001fa00—FUN_10001fa00AFM-emit happy path (post-gate-pass).0x10001ad60—FUN_10001ad60invalidation handler (callsreleaseAssertion).
Calibration fix vs D45 anchor¶
The "Cannot acquire a usage assertion on a device without an
effective device identifier." string lives at slice VA
0x1000b7d30 (slice file offset 0xb7d30). D45's anchor row
CDS+0xbbd30 was the FAT-archive-relative offset; the x86_64
slice starts at FAT offset 0x4000, so subtract 0x4000 to convert
FAT → slice. The other strings D45 listed (0xb7d90 subsystem
string, etc.) were already slice-relative and remain correct.
D46's Q1 also revealed why direct-LEA xref scans returned 0 hits in
D45: the Swift compiler packs __cstring densely and constructs
this error message at runtime via Swift String length-prefixed
encoding from a SHARED SUFFIX 0x1000b89e0 ("an effective device
identifier."). 8 LEAs into the suffix appear across 4 sibling
gates (DDI-disable, DDI-enable, Pair, Tunnel-create) plus the AUA
path itself — confirming the same _get_effectiveDeviceIdentifier
gate fires uniformly across CDS for all SDR-dependent action
handlers.
Strong CDS-side strings on AUA failure path (D46-corrected)¶
| Slice VA | String |
|---|---|
0x1000b7d30 |
"Cannot acquire a usage assertion on a device without an effective device identifier." — confirmed gate-trip via _get_effectiveDeviceIdentifier == .none predicate in FUN_100020700 |
0x1000b7d90 |
subsystem string com.apple.dt.coredevice.useassertion. |
0x1000b8030 |
"This device does not support acquiring a usage assertion." — separate capability gate elsewhere |
0x1000b7ef0 |
"Acquired usage assertion." — success log line |
0x1000b89e0 |
shared suffix "an effective device identifier." used by 4 sibling gates (DDI-disable/enable, Pair, Tunnel-create) plus AUA |
0x1000bbb20 |
sibling at distinct prefix; confirmed not-AUA |
"Effective device identifier" semantics¶
ServiceDeviceRepresentation._effectiveDeviceIdentifier:
Optional<DeviceIdentifier> is a Swift property on the CDS-internal
SDR populated post-RSD-pair. Distinct from the Context.deviceIdentifier
UUID propagated via Mercury XPC (the action's inbound deviceID) and
distinct from RemoteDevice.deviceIdentifier (pre-pair UUID).
Returns .none when:
- (a, most likely) the SDR for the requested UUID is registered but its post-pair RSD lookup hasn't populated the field yet.
- (b, less likely) explicit invalidation marker is set.
iosmux's existing inject chain (MDRemoteServiceSupport,
iosmux_md_proxy.m, serviceDeviceRepresentations interpose)
supplies device data into CD-side / devicectl-side structures but
does NOT touch the CDS-side SDR's _effectiveDeviceIdentifier
field. This is a separate state surface that iosmux has not yet
touched — explaining why all 8 prior falsifications missed the real
gate.
CDS-internal call chain (D46-traced)¶
[Mercury action dispatcher]
└─ FUN_100018ea0 (action router)
└─ FUN_100020700 (AUA action `run` body, slice VA 0x100020700)
├─ Stage 1: type metadata accessor instantiation
├─ Stage 2: extract Context::deviceIdentifier (UUID)
├─ Stage 3: CoreDeviceService::_serviceDeviceRepresentation(UUID, conformingTo)
├─ Stage 4: GATE — _get_effectiveDeviceIdentifier(this, out)
│ ├─ if .none → CoreDeviceError.deviceRepresentationMissingIdentity
│ │ → _Continuation._resume(error) [errorCode 3]
│ └─ if .some → fall through
├─ Stage 5: extract Input.{reason, options, endpoint}
└─ Stage 6: dispatch to FUN_10001fa00 (AFM-emit happy path)
├─ xpc_connection_create_from_endpoint (CDS-side side-channel
│ construction; confirms D42 Q2 architectural inversion)
├─ InProgressServerAssertion alloc + queue setup
├─ Mercury::SystemXPCPeerConnection::_setEventHandler
└─ Mercury::XPCConnection::_send (AssertionFulfilledMessage)
InProgressServerAssertion lifecycle — NOT in gate path¶
D46 Q5: the Obj-C class CoreDeviceService.InProgressServerAssertion
(at CDS+0x1000b6386) is the server-side per-assertion-bookkeeping
object. It is allocated in FUN_10001fa00 AFTER the gate at Stage 4
passes — it does NOT exist yet at gate-trip time. Hooking IPSA's
construction or its IVAR getters does not help; the gate is upstream.
Field layout (IVARs at 0x1000d2048, base struct at 0x1000d2e68):
| Offset | Name | Type |
|---|---|---|
+0x3420 |
assertionIdentifier |
UUID |
+0x3428 |
peerConnection |
Mercury.SystemXPCPeerConnection |
+0x3430 |
deviceIdentifier |
DeviceIdentifier |
+0x3438 |
serviceDeviceRepresentation |
ServiceDeviceRepresentation |
+0x3440 |
requiredPreparedness |
DevicePreparedness |
+0x3448 |
assertionDetails |
IdentifiableAssertionDetails |
+0x3450 |
invalidationHandler |
closure |
+0x3458 |
invalidated |
AtomicBool |
+0x3460 |
queue |
OS_dispatch_queue |
+0x3468 |
devicePowerAssertion |
(per FUN_10001ad60 access) |
D46b-resolved hook target — effectiveDeviceIdentifier.getter dispatch thunk¶
Direction F is bridgeable because the dispatch thunk for
_get_effectiveDeviceIdentifier is an EXTERNAL symbol in
CoreDevice.framework. CDS imports the thunk by name (chained-fixup
name_offset = 29559); our existing iosmux_inject.dylib is loaded
into the same CDS process. The thunk is reachable via standard
dlsym(RTLD_DEFAULT, "<mangled>") from inside the inject. No new
injection target needed.
| Property | Value (D46b-confirmed) |
|---|---|
| Target | dispatch thunk of CoreDevice.ServiceDeviceRepresentation.effectiveDeviceIdentifier.getter |
| Mangled symbol | _$s10CoreDevice07ServiceB14RepresentationC09effectiveB10IdentifierAA0bF0OSgvgTj |
| Demangled | dispatch thunk of CoreDevice.ServiceDeviceRepresentation.effectiveDeviceIdentifier.getter : CoreDevice.DeviceIdentifier? |
| Slice VA in CoreDevice | 0x28db10 (x86_64 slice) |
| Symbol kind | T (text, externally exported); bare getter vg is NOT exported — only the thunk vgTj is reachable from outside the CoreDevice image |
| Type kinds | ServiceDeviceRepresentation is class (C) — not protocol; DeviceIdentifier is enum (O) — not struct (D46 candidate symbol used P V — both wrong) |
| Reachability | dlsym(RTLD_DEFAULT, "<mangled>") returns thunk address inside loaded CoreDevice image |
| Calling convention | Swift class-method ABI on x86_64 — non-cdecl: R13 = self (SDR class instance), RAX = indirect-result buffer pointer for Optional<DeviceIdentifier> return value. Hook entry MUST use inline-asm preamble to capture R13/RAX before any C code runs. |
| Prologue | 15 contiguous clean linear bytes before the JMP RCX terminator: PUSH RBP / MOV RBP,RSP / MOV RCX,[R13] / MOV RCX,[RCX+0xd8] / POP RBP. Sufficient headroom for a 12-byte absolute-jump trampoline (MOV RAX,imm64 + JMP RAX). No PC-rel, no PAC. |
| Trampoline | S1.B pattern proven at inject/iosmux_inject.m:1463-1525 — 15 saved bytes |
Critical correction vs D46's hook recipe¶
D46 proposed if (out[16] == 1) { memcpy(out, uuid, 16); out[16] = 0; }.
This is wrong. D46b empirical disasm at CDS:0x100020d20 shows
Swift uses runtime VWT calls (getEnumTagSinglePayload at VWT
slot +0x30) to read the Optional tag — there is NO fixed byte-tag
offset. The Optional
Hook payload — capture-and-replay strategy (layout-agnostic)¶
D46c MUST use either Swift VWT machinery OR a capture-and-replay strategy that avoids touching the Optional buffer's tag byte directly. Recommended (simpler, more robust):
- At inject load,
dlsymthe thunk symbol. Install S1.B trampoline at thunk address (15-byte saved prologue). - On every hook entry, capture R13 (self) + RAX (indirect-result) via inline asm. Call original via the saved trampoline.
- After original returns, inspect the Optional tag via
getEnumTagSinglePayload(VWT slot+0x30). - If
.some(tag == 0): if first time seen on a sibling-gate call (DDI-disable/enable, Pair, Tunnel-create),memcpythe buffer to a globalg_known_some_bufferstatic — captured layout-agnostic. Always pass through. - If
.none(tag == 1) AND we've captured a.somebuffer: destroy current.nonepayload via VWT slot+0x08(destroy), theninitializeWithCopyfromg_known_some_buffervia VWT slot+0x10. Optional now reads as.somewith valid DeviceIdentifier — gate passes. - If
.noneAND no.somecaptured yet: passthrough; gate trips this time. Subsequent calls will replay once a sibling has provided a known-good buffer.
This avoids constructing DeviceIdentifier.uuid(...) from scratch
(which would require knowing the case-constructor's mangled symbol
and Swift String layout) — instead, it reuses an Apple-constructed
buffer that Swift runtime is guaranteed to accept.
Major risks (D46b risk register R1, R5-R11)¶
| ID | Risk | Mitigation |
|---|---|---|
| R1 | Thunk fires for ALL 4 sibling gates (DDI-disable/enable, Pair, Tunnel-create) plus AUA | Stack-frame return-address discriminator at hook entry: only override when caller is FUN_100020700 (CDS:0x100020700..0x1000218fc); passthrough for sibling gates. ~10 lines C. |
| R5 | Optional tag is NOT at fixed byte offset — uses Swift VWT runtime tag access | Use VWT machinery (getEnumTagSinglePayload slot +0x30) OR capture-and-replay (preferred — layout-agnostic) |
| R6 | Non-cdecl calling convention (R13=self, RAX=indirect-result) |
Inline-asm preamble at hook entry to capture R13/RAX before C code runs |
| R7 | Capture-and-replay needs a known-good .some buffer to bootstrap |
Capture from first natural .some on a sibling gate during normal Xcode flow before AUA's first attempt — sibling gates routinely succeed in non-iosmux flows |
| R8 | Hook recurses if buffer construction triggers another effectiveDeviceIdentifier call |
Per-thread in_hook flag for recursion passthrough |
| R9 | dlsym(RTLD_DEFAULT, <Swift mangled>) may behave unexpectedly |
Verify at D46c init; fallback to manual exports-trie walk via _dyld_get_image_* if dlsym returns NULL |
| R10 | Constructing DeviceIdentifier from scratch needs case-constructor mangled symbol | Capture-and-replay (TODO-A) sidesteps this |
| R11 | selfReportedDeviceIdentifier (sibling at 0x28a440/thunk 0x183510) might be a fallback target if effective proves unreplaceable |
Documented; primary remains effective thunk |
Alternative hook targets — REJECTED¶
- Target #2 —
FUN_100020700whole-prologue replacement: 4.6 KB body re-implementation; high Swift-runtime fragility; would have to re-build IPSA + Mercury wrapper from scratch. - Target #3 —
_get_deviceRepresentationMissingIdentityconstructor hook: wider blast radius (catches all callers across CD/CDS); throw suppression at constructor time is harder than short-circuiting the predicate. - Witness-table slot direct hook —
ServiceDeviceRepresentationis a class (vtable dispatch), not a protocol (witness-table dispatch); the thunk IS the vtable-dispatch entry, no separate witness slot exists. - Bare getter at vtable slot — not externally exported; would require runtime metadata walk and would have only ~6 clean prologue bytes (PC-rel-heavy, UNSAFE for trampoline).
D46c + D46c-fix outcome — BLOCKED on Swift metadata accessor resolution¶
D46c (c-developer impl dispatch) authored the full hook code in
inject/iosmux_aua_edi_hook.{h,m} per D46b spec — naked-asm
wrapper, capture-and-replay body, S1.B trampoline install,
file-presence gate, return-address discriminator, recursion guard.
Build clean, codesign clean, deploy clean, CDS-stable post-respawn.
Hook code is dormant + ready in tree (commit pending); install
function bails before arming the trampoline because the
_$s10CoreDevice16DeviceIdentifierOMa type metadata accessor cannot
be resolved at runtime.
D46c-fix (c-developer delta dispatch) added a Mach-O LC_SYMTAB
walker as fallback (5281-entry per-image nlist scan with
__LINKEDIT mapping) — empirically blank for the target symbol.
Empirical learning — Tahoe 26.x dyld shared cache strips local
symbols. Both dlsym(RTLD_DEFAULT, ...) (exports trie) AND
direct nlist_64 walk (per-image LC_SYMTAB) miss internal Swift
symbols (e.g. type metadata accessors _$s..OMa for non-public
types) for any framework loaded from the shared cache. The dyld
shared cache builder consolidates local symbols into a separate
cache-side symbol-info file. Captured in memory rule
feedback_dyld_shared_cache_strips_symbols.
D46c authored code paths (kept in tree, ready to activate):
| Component | Status |
|---|---|
inject/iosmux_aua_edi_hook.h |
API: single iosmux_aua_edi_hook_install(void) |
inject/iosmux_aua_edi_hook.m (~433 lines + ~100 lines symtab walker) |
Naked-asm wrapper, C body with capture-and-replay, S1.B trampoline install, AUA caller-discriminator, recursion guard, file-presence gate, dlsym + symtab fallback |
inject/iosmux_inject.m |
#include + iosmux_aua_edi_hook_install() call at end of constructor |
inject/Makefile |
source list updated |
Hook entry log D46c: hook installed at thunk 0x... |
Never fired (install bails at metadata accessor) |
| Apparatus | D23 v2 baseline 52df2cc6...4803 restored after each cycle; ZERO regressions through D46c+fix |
Forward paths — pick #2 or #3¶
Two paths remain to give the hook a usable metadata accessor; once either lands, the existing hook code activates with a one-line patch in the install function.
Path #2 — Hardcoded metadata-accessor offset (Ghidra-derived). D46d
RESOLVED 2026-05-03: slice VA / image-base-relative offset
0x00259910 for the metadata accessor in
CoreDevice.framework x86_64 slice (sha
bea205e2c64622d144bcc7664ee104083d0e192aca206739cca345dc7c420495,
matches D21/D22 baseline). Approach A (Ghidra D36 query, 4-pass
verification chain via xref + disasm + convention check + label
inspection) succeeded; havoc shared-cache extraction NOT triggered.
Mangling subtlety: the canonical mangled form Apple's strip
pipeline preserves is _$s10CoreDevice0B10IdentifierOMa (with Swift
identifier-substitution compression: 0B = back-reference to
identifier index 0). The expanded form
_$s10CoreDevice16DeviceIdentifierOMa D46c tried via dlsym does
not exist in any symbol table — this contributed to the D46c+fix
block alongside the shared-cache stripping.
D46e patch shape (ready) — see notes/d46d-metadata-offset.md for full code, summary:
#define IOSMUX_AUA_CD_DID_METADATA_OFFSET ((uintptr_t)0x00259910)
static void *iosmux_resolve_did_metadata_accessor(void) {
// Walk loaded image list for /CoreDevice.framework/Versions/A/CoreDevice
// (mirrors iosmux_resolve_remove_cached_xpc in iosmux_aua_keepalive.m).
// Return _dyld_get_image_header(CoreDevice_idx) + offset.
}
The accessor signature is Metadata *(MetadataRequest) — RDI = request,
returns {Metadata*, MetadataState} in {RAX, RDX}.
D46e is the c-developer dispatch replacing the existing dlsym/symtab resolution path with this hardcoded resolution.
Path #3 — Dynamic metadata recovery via Swift runtime on first
hook entry. Defer metadata resolution to runtime: the hook
captures R13 (self) on first call, walks *self → metadata
pointer → vtable → ... to recover Optional<DeviceIdentifier>'s
metadata indirectly. Bypasses the static symbol-resolution problem
entirely. More fragile (requires Swift runtime helper calls or
direct field-offset arithmetic); version-resilient (no hardcoded
offsets). Estimated 50-100 lines of Swift-runtime-aware C.
D46d-alt would be a single c-developer dispatch authoring the runtime metadata walker + integrating it as the new resolution path.
Trade-off (path #2 vs #3)¶
- Path #2 is faster to land (1 research probe + 1 small impl patch = 2 dispatches; ~30min total agent time).
- Path #3 is more maintainable long-term (no offset re-derivation per Apple version) but higher implementation risk (Swift runtime ABI is undocumented for these private types).
- Project precedent (
iosmux_aua_keepalive.m) uses path #2 for the existing CD-side NOP-evict hook — same pattern, same approach, known-working at apparatus level.
Recommended: path #2 first. If maintenance burden materializes across multiple Tahoe point updates, switch to path #3 in a later iteration.
Why this finally found the gate but blocked on the implementation¶
D46 + D46b correctly identified the gate (server-side
_get_effectiveDeviceIdentifier == .none) and the right hook
target (the dispatch thunk in CoreDevice). D46c's implementation
was clean and CDS-stable. The block is purely on a tooling problem
specific to Apple's modern shared-cache symbol stripping — not on
any architectural mistake. Both surviving paths bypass the strip
entirely.
Why this finally found the gate after 8 falsifications¶
All prior probes (D23..D45) operated on CD-side / devicectl-side /
Mercury-side surfaces because the failure presentation
([useassertion] Failed to acquire ... errorCode 3 arriving at
the awaiter) suggested a client-observable mechanism. D45 Q4
inverted that assumption empirically (subsystem string only in
CDS), and D46 followed the trail into the CDS-internal action
implementation where the predicate actually lives. The gate is
neither a connection lifetime issue (D23..D42 Q3), nor a Codable
shape issue (D43..D44b'), nor a UUID-comparison issue (D45 D30 #1)
— it is server-side device-record state validation that
iosmux has never touched.
DO NOT RETEST (do not waste session time)¶
The following hypotheses are empirically dead. Future sessions that re-derive them are wasting time. Do not propose:
- AFM byte-shape iter-½/3 (D20) — mangledTypeName / shape / identifier all falsified.
- Cache-eviction (D23) — NOP'd at CD+0xdbe0; eliminated 1001 on action-XPC; residual errorCode 3 still fires. Hook still deployed as baseline.
- AFM peer cancel-after-send (D30) — peer lifetime irrelevant.
- Listener Swift wrappers (D38 G-1 / G-1') — Mercury+0x4e930 / +0x4e960 do NOT hold send right.
- AUA-completion context retain (D38 G-4) —
lVar17+0x18NOT a strong reference. - Peer Swift wrapper (D41 G-2) — Mercury+0x4ea10 does NOT hold send right either.
- CDS-side app-code cancel hook (D42 Q3) — zero callers in CDS application code; lifecycle Swift-ARC mediated only.
- State conditioning (D40 P1) — apparatus state (S1/S2/S3) does NOT influence outcome (66/66 Path-B deterministic).
- DSS type identity / CodingKeys (D43) — TMA
CD+0x29bb30ISCoreDevice.DeviceStateSnapshot; 3 CodingKeys match GAMBIT emit exactly. capabilityImplementationsempty-vs-non-empty (D44b') — empty IS decoder-tolerated; non-empty produces byte-identicalerrorCode 3outcome.- AFM identifier mismatch (D45 = D30 hypothesis #1) — no separate
per-assertion-instance UUID exists on the AUA endpoint side-channel
path. AUA wrapper async body (
CD+0x317a0) and completion-handler body (CD+0x304a0) call zero UUID constructors. CD-side AFM-decode handler atCD+0x2fb30..0x2ff3dcopies AFM.identifier byte-for-byte into DUA.identifier with no equality compare. AUA actionInputhas only 3 fields (reason,options,endpoint) — no per-assertion UUID on the wire either. GAMBIT's current echo ofdeviceIdentifieris correct.IdentifiableAssertionDetails(CD+0x290330) exists but is on the parallelTunnelAssertionRequest.identifiabletunnel-management API, not on AUA. - devicectl-side
useassertionemission (D45 Q4 architectural pivot) — subsystem stringcom.apple.dt.coredevice.useassertion.lives ONLY inCoreDeviceService(CDS+0xb7d90 / VA0x1000b7d90), NOT in CD/devicectl/CDU/Mercury. The pre-D45 framing of "non-Codable devicectl-side state-machine check" is wrong about the emission process. Surviving candidate is CDS-side — D46 scope. launchctl setenvfor CDS-side runtime gates (D44b apparatus learning) — env vars do NOT propagate to CDS XPC service post-respawn. Cause unknown (NOT SIP — SIP is off). Use file-presence gate or unconditional patch instead.
Side-channel manifestation under different timing (Heisenbug)¶
D34/D35a + D37-E established that lldb / dtrace instrumentation alters the AUA failure path itself — not just timing. Two externally-visible failure shapes:
Path-B (production timing, D29 0/30 + D40 66/66)¶
11 ms gap from success(...) reply received → errorCode 3
emitted. No Successfully acquired log. No XPCError 1001
peer[2019] log line in current apparatus (D37-G observation —
the 1001 surfacing seen in D24 era was caused by the
cancel-after-send pattern that D30 eliminated).
HH:MM:SS.xxx488 Received XPC reply: success(...) [action channel]
HH:MM:SS.xxx044 forward(...): success()
HH:MM:SS.xxx754 Failed to acquire usage assertion ...
CoreDeviceError(errorCode: 3,
errorUserInfo: ["NSLocalizedDescription":
"Failed to acquire assertion"])
Path-A (lldb-perturbed, D31 Run #2 + historical D24 era only)¶
38 ms gap; wrapper briefly observes "Successfully acquired" before
invalidation; side-channel XPCError 1001 log fires explicitly
before the assertionq throw. Apparently winnable race under
debugger timing only — D40 P1 confirmed Path-A is NOT
state-conditional and does not reproduce in production timing.
Implication: any future probe must use Heisenbug-immune
methods (static disasm, in-process JSONL counter-instrumentation,
or os_signpost USDT) — not lldb / dtrace BP-based attach.
Throw chain (D22-era technical map, partially superseded)¶
Frame chain at the throw under production timing:
frame #0: libswift_Concurrency.dylib`swift_continuation_throwingResume
frame #1: CoreDevice`___lldb_unnamed_symbol_324a0 + 98 (CD+0x32502) ← async-continuation completer
frame #2: CoreDevice`___lldb_unnamed_symbol_3bc80 + 61 (CD+0x3bcbd) ← type-metadata thunk
frame #3: CoreDevice`___lldb_unnamed_symbol_30eb0 + 2034 (CD+0x316a2) ← C-ABI result dispatcher (success-path RetPC)
frame #4: CoreDevice`___lldb_unnamed_symbol_1e300 + 25 (CD+0x1e319) ← Swift closure-invocation trampoline
frame #5: libdispatch.dylib`_dispatch_call_block_and_release + 12
frame #6..n: libdispatch lane-drain + workloop worker thread
Throw runs on a dedicated NAMED serial dispatch queue
(com.apple.dt.coredevice.remotedevice.default.assertionq), NOT
on the original Task 1 cooperative thread. The dispatcher chain
___30eb0/___3bc80/___324a0 is a shared funnel with at
least two empirically-distinct input paths (D24 W4) — it cannot be
hooked usefully because cutting one input doesn't address the
other. Per D42, these inputs are now understood to all eventually
trace back to the AFM-decode-failure → Result.failure →
continuation.throw cascade rather than the cache-eviction
mechanism D22 proposed.
taskHeapMetadata + 24 (0x00007ffe42111e70 in D21 capture) is
NOT a CoreDeviceError vtable but libswift_Concurrency.dylib
runtime metadata for task-heap-allocated objects (D22 Q4
correction). Error type identity is in object payload fields, not
the heap header.
Apparatus baseline¶
- iosmux_inject.dylib: D23 v2 sha
52df2cc6...4803deployed in CDS. - New
iosmux_devicectl_inject.dylib: opt-in via wrapper, currently G-2 shaab178ee5...172a4(FALSIFIED, retained for reference; no effect on naked devicectl invocations). iosmux_xcode_inject.dylib: D39 H#2 build deployed but NOT activated (Xcode integrity incident gate; future state-tagged retest gated on user-driven recovery confirmation).- All Apple binaries unchanged from
recovery-2026-05-02/baseline (sha-verified post-D39 incident).
D21 + D22 + D23 + D24 + D30 + D31 + D32 + D33 + D34 + D35a + D36 +
D37-* + D38 + D39 + D41 + D42 + D43 + D44a + D44b + D44b' + D45 all
ran read-only on Apple binaries. D23 + D30 + D38 + D39 + D41 + D44b
+ D44b' modified iosmux-controlled dylibs only (D44b/D44b' restored
via backup post-probe). D43 + D44a + D45 were pure host-side static
disasm — zero apparatus interaction. Zero binaries modified outside
iosmux scope. Zero CDS or devicectl crashes during any probe.
Pre-attach manifest at
/home/op/backups/iosmux/d21-pre-attach/manifest-source.txt.
See also¶
docs/plans/d66-research-questions.mdQ-D66-15 — current status row + cross-link to history archives.mercury-action-interceptor-design.md— GAMBIT design baseline; this doc complements it by clarifying that AUA reply alone is necessary but not sufficient, and the AFM payload fidelity is the open question.gambit-pair-action-schema.md— current AFM payload structure (the candidate type-mismatch source for Direction D).pair-button-and-cfnetwork.md§"Two gates, not one" —_shadowUseAssertion.fulfilledXcode UI gate that AUA success would feed into, currently blocked by the AFM-decode-failure cascade.../../runbooks/state-tagged-probe-template.md— required Dimension 1-8 capture for any forward AUA probe.- ADR-0009 §Decision — bridge-not-proxy architecture; Direction D stays within scope.
- ADR-0006 — empirical-only discipline; every claim above is sourced from a specific probe entry in the history archives.