Skip to content

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:

  1. Action channel — the inject-synthesised success(...) reply that GAMBIT already produces (Phase 1 baseline). The action reply lands and is accepted.
  2. 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.endpoint of the AUA action invocation and constructs its peer half via xpc_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):

  • 0x100020700FUN_100020700 AUA action run body (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.
  • 0x10001fa00FUN_10001fa00 AFM-emit happy path (post-gate-pass).
  • 0x10001ad60FUN_10001ad60 invalidation handler (calls releaseAssertion).

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 layout depends on spare-bits encoding which cannot be statically determined without disassembling DeviceIdentifier's nominal type descriptor + VWT.

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

  1. At inject load, dlsym the thunk symbol. Install S1.B trampoline at thunk address (15-byte saved prologue).
  2. On every hook entry, capture R13 (self) + RAX (indirect-result) via inline asm. Call original via the saved trampoline.
  3. After original returns, inspect the Optional tag via getEnumTagSinglePayload (VWT slot +0x30).
  4. If .some (tag == 0): if first time seen on a sibling-gate call (DDI-disable/enable, Pair, Tunnel-create), memcpy the buffer to a global g_known_some_buffer static — captured layout-agnostic. Always pass through.
  5. If .none (tag == 1) AND we've captured a .some buffer: destroy current .none payload via VWT slot +0x08 (destroy), then initializeWithCopy from g_known_some_buffer via VWT slot +0x10. Optional now reads as .some with valid DeviceIdentifier — gate passes.
  6. If .none AND no .some captured 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 #2FUN_100020700 whole-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_deviceRepresentationMissingIdentity constructor 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 hookServiceDeviceRepresentation is 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+0x18 NOT 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+0x29bb30 IS CoreDevice.DeviceStateSnapshot; 3 CodingKeys match GAMBIT emit exactly.
  • capabilityImplementations empty-vs-non-empty (D44b') — empty IS decoder-tolerated; non-empty produces byte-identical errorCode 3 outcome.
  • 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 at CD+0x2fb30..0x2ff3d copies AFM.identifier byte-for-byte into DUA.identifier with no equality compare. AUA action Input has only 3 fields (reason, options, endpoint) — no per-assertion UUID on the wire either. GAMBIT's current echo of deviceIdentifier is correct. IdentifiableAssertionDetails (CD+0x290330) exists but is on the parallel TunnelAssertionRequest.identifiable tunnel-management API, not on AUA.
  • devicectl-side useassertion emission (D45 Q4 architectural pivot) — subsystem string com.apple.dt.coredevice.useassertion. lives ONLY in CoreDeviceService (CDS+0xb7d90 / VA 0x1000b7d90), 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 setenv for 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.failurecontinuation.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...4803 deployed in CDS.
  • New iosmux_devicectl_inject.dylib: opt-in via wrapper, currently G-2 sha ab178ee5...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