Skip to content

AUA history — D36..D37 host-side static disasm era (2026-05-01..2026-05-02)

Status: verified — historical archive

Host-side Ghidra + radare2 + llvm-objdump static analysis after the D35a Heisenbug invalidated lldb/dtrace dynamic probes. Maps the Path-A/Path-B chain end-to-end, then iterates trigger localisation through D37-C..D37-G. Archived from aua-side-channel-mechanism.md on 2026-05-02 anchor refactor.

D37-G update: CDS-side counter-instrumentation deployed cleanly; Path-A NOT REPRODUCED (0/5 runs + 0/24h history); peer teardown sub-ms; current apparatus state SUPPRESSES D24-era XPCError 1001 sentinel; §D-1 indeterminate (effect captured, cause requires devicectl-side probe) (2026-05-02)

D37-G extended iosmux_inject.dylib with ADDITIVE-only AUA peer lifecycle logging (new file inject/iosmux_aua_peer_logger.m, ~480 lines, 2 new DYLD_INTERPOSE entries on xpc_connection_cancel and xpc_release, gambit hook points for create/set_handler/activate/send/event). Built cleanly on havoc, deployed, captured 5 AUA invocations, restored D23 v2 baseline. Zero crashes, zero behavior change verified.

Key empirical findings

Sub-ms peer teardown timeline (5 runs avg, all Path-B):

t=0       gambit synthesizes success() reply on action XPC
t+~10ms   gambit dials devicectl's listener (peer create on CDS-side)
t+~11ms   gambit sends AFM (CoreDevice.AssertionFulfilledMessage)
t+~12ms   libxpc delivers "Connection invalid" event to peer (171-872 µs after send)
t+~13-17ms devicectl "Failed to acquire usage assertion" user-visible (0.5-5 ms after libxpc event)

Peer create→event delta: 171-872 µs. ⅕ runs received Connection INTERRUPTED first then Connection INVALID (172 µs apart) — listener-side graceful tear-down. Other ⅘ went straight to INVALID — likely NO_SENDERS arriving in single mach event.

Path-A not reproduced: 0/5 runs + 0/24h history. Matches D29's established 0/30 baseline. This means brief's primary Q-G-1 (cancel BEFORE or AFTER "Successfully acquired") cannot be answered — there's no Path-A signal to compare against.

Surprising state finding: D24-era log line "Recieved error from side channel peer: XPCError(errorCode: 1001 ... peer[2019])" does NOT fire in current apparatus. The current Path-B failure surface is just useassertion: Failed to acquire usage assertion. The numeric 1001/3001 codes are constructed at higher Mercury/Swift error layers in devicectl, not at the libxpc surface CDS-side observes.

Same-image DYLD_INTERPOSE limitation reconfirmed

DYLD_INTERPOSE only rewrites GOT entries in OTHER images. Calls within iosmux_inject.dylib itself (gambit's send_barrier { xpc_connection_cancel(ep_conn) } + xpc_release(ep_conn)) go direct to libxpc and bypass our interpose. So D37-G captured ZERO gambit-side cancel/release events — only Apple-framework cancel/release on the tracked AUA peer would be visible (none captured = consistent with D37-F finding that no Apple framework code explicitly cancels).

To capture gambit's own cancel/release timing, instrumentation must be inline in gambit (calling logger function directly), not via DYLD_INTERPOSE. Easy follow-up if needed.

§D-1 status: INDETERMINATE (was: NOT REPRODUCED)

CDS-side instrumentation observes EFFECT (peer's "Connection invalid" sentinel arriving via libxpc) not CAUSE (devicectl-side ARC release of listener). The libxpc sentinel arrives in BOTH Path-A and Path-B cases — listener teardown is post-reply in either. Distinguishing requires Path-A run for direct comparison.

What's needed for definitive §D-1 answer

Two paths remain:

  1. Path-A reproduction in current D23 v2 apparatus — rare (0/30 historical), unlikely without apparatus changes
  2. devicectl-side counter-instrumentation (D37-H?) — separate iosmux_devicectl_inject.dylib per D33 P3 feasibility, hooks Mercury.SystemXPCListenerConnection.deinit at Mercury+0x4e930 directly with in-process logging. Heisenbug-immune (no debugger). This empirically validates §D-1 by observing the deinit firing AND its effect (peer cancel) within same process timeline.

Working-tree state (uncommitted)

D37-G left additive source-tree changes:

  • NEW inject/iosmux_aua_peer_logger.m (~480 lines, untracked)
  • MODIFIED inject/iosmux_gambit.m — 1 forward-declare + 4 call sites
  • MODIFIED inject/iosmux_inject.m — 1 forward-declare + 1 init call
  • MODIFIED inject/Makefile — append new .m

Deployed dylib on havoc is BASELINE (D23 v2 sha 52df2cc6...4803). Source tree changes are research artifacts; user can commit or revert.

Apparatus integrity (D37-G also non-destructive)

  • iosmux_inject.dylib on havoc: sha 52df2cc6...4803 (D23 v2) ✓ restored
  • iPhone (iosmux) connected (no DDI)
  • CDS PID 13012 stable (was 1013, restarted via killall during deploy)
  • Secondary CDS 10797 still running (baseline drift, non-destructive)
  • Zero new DiagnosticReports
  • Pre-deploy snapshot at /home/op/backups/iosmux/d37g-pre-deploy/iosmux_inject.dylib.d23v2-presnapshot

Distillation status

D37-G raw notes (notes/d37g-cds-peer-lifecycle.md, ~530 lines) deleted at distillation time per feedback_notes_are_temporary_buffer. Captured artifacts preserved at /home/op/dev/iosmux/symbols/d37g-aua-peer-events.jsonl (26 events across 5 runs) and d37g-unified-log.txt (host-side, not in repo). Verdict + sub-ms timing + working-tree state preserved in this section + Q-D66-15 D37-G Resolution log entry.

D37-F update: cross-binary trigger search FALSIFIES Hypothesis B; D37-D's hook target SURVIVES; new G-4 alternative identified (extra retain on AUA-completion context) (2026-05-02)

D37-F extended D36's Ghidra project with libdispatch from havoc's dyld_shared_cache (libxpc extraction failed due to ipsw/Sequoia 26.x DSC format incompatibility — failed to rebase dylib via cache slide info: invalid command block size; mitigation: read libxpc symbol table via ipsw dyld macho --symbols, no Ghidra disassembly).

Searched all 4 client binaries + libdispatch for explicit cancel/release callers reachable from AUA flow.

Headline result: Hypothesis B FALSIFIED, D37-D survives

ZERO direct callers of xpc_connection_cancel, xpc_remote_connection_cancel, dispatch_mach_cancel, _xpc_connection_remove_peer*, _xpc_listener_cancel, _xpc_session_cancel, _xpc_connection_close, _xpc_connection_send_termination_event, _xpc_pipe_invalidate exist in CD / CDU / Mercury / devicectl reachable from CD+0x304a0 (AUA async wrapper) or its closure tree [CD+0x30000, CD+0x32000).

Only 3 cancel-method-impl trampolines exist anywhere in the process image, all with ZERO callers in the AUA flow:

  • Mercury+0x4e589 — SystemXPCConnection.cancel impl (dispatch thunk)
  • Mercury+0x3d239 — RemoteXPCConnection.cancel impl
  • CDU+0x1509b9 — likely DTRS-service-client cancel (not on AUA path)

All 20 Mercury class deinit bodies enumerated; NONE contains an explicit XPC cancel call. The strongest cleanup candidate remains Mercury.SystemXPCListenerConnection.deinit at Mercury+0x4e930 doing _swift_unknownObjectRelease(0x18(%r13)) on the listener's OS_xpc_connection — i.e. D37-D's primary hook target.

Verdict on Hypothesis B (D37-E §"Alternative trigger candidates")

EMPIRICALLY FALSIFIED: there is NO app-level code path in the 4 binaries that calls cancel/teardown family functions directly. All XPC connection teardown in the AUA flow is purely ARC-driven: a Swift class deinit's _swift_unknownObjectRelease(OS_xpc_connection) drops libxpc's last strong reference, libxpc's own _xpc_connection_dispose + _xpc_connection_remove_peer_impl chain handles peer-table walk and MACH_NOTIFY_NO_SENDERS propagation kernel-side without app-level help.

This collapses Hypothesis B into Hypothesis A (Heisenbug suppresses full Path-A under instrumentation) — no third path remains.

Verdict on Hypothesis C (CDS-side first-drop)

INDETERMINATE — no CDS-targeting cancel-message in CD/CDU/Mercury/devicectl identified, but cannot be definitively ruled out without separate CDS-side Ghidra analysis. Mercury imports common XPC send paths (_xpc_connection_send_message, _with_reply, _with_reply_sync) but none are inherently cancelling. XPCError.terminationImminent exists as a Swift error type at Mercury+0x57dc0 but Mercury only RECEIVES this signal; never sends it.

NEW hook target G-4 — extra retain on AUA-completion context

D37-F surfaced a NEW alternative cause-side hook that doesn't require stack-walking discrimination:

Hook the allocation site at CD+0x304a0+0x186 (the _swift_allocObject(...lVar17) call that creates the AUA-completion context), insert an extra _swift_retain(lVar17) after construction. This keeps the context alive forever — Path-A's natural ARC release of lVar17 fails to drop refcount to 0, listener wrapper never deinits, no _swift_unknownObjectRelease(listener_xpc), no NO_SENDERS, no HIT #18, Path-B never fires.

Mechanism: prologue-patch CD+0x304a0 around offset +0x18a (just after the alloc call), insert ~10 bytes that load the alloc result and call _swift_retain on it before continuing. NO stack-walking discriminator needed because we hook the AUA wrapper itself directly.

Confidence: MEDIUM — mechanically clean, no false-positives possible (only fires when AUA is invoked), no Heisenbug interaction with timing.

Side effect: leaks one ~0x40-byte AUA-completion-context heap object + 1 anonymous listener OS_xpc_connection + associated mach ports per AUA invocation. devicectl is short-lived (one CLI command) — bounded leak.

Tradeoff vs G-1: G-4 is structurally simpler (no stack walk, no per-deinit filter), more deterministic, AUA-only by construction. G-1 is more general (could in principle apply to other actions later) but requires runtime stack discrimination that D37-E couldn't empirically test.

G-1 status post-D37-F

D37-D's primary recommendation remains the only Swift-level code site that performs _swift_unknownObjectRelease(listener_xpc) in the AUA flow. D37-E's Heisenbug-caveat verdict still stands (couldn't observe firing in 6 dtrace runs), but D37-F's failure to find any other candidate strengthens G-1's standing — there's nothing else it could plausibly be.

Other candidates (anti-recommendations)

  • G-2 libxpc-internal _xpc_connection_remove_peer_impl — UNVIABLE: requires DSC slide arithmetic, function is private (non-external), libxpc loads before DYLD_INSERT_LIBRARIES injection. Hooking system-shared functions affects every process. Mentioned for completeness only.
  • G-3 FUN_3b090 helper called from listener deinit + peer deinit — body not yet disassembled. If it turns out to be a critical ARC-bridge or port-deactivation helper, could become viable target. Empirical follow-up needed.
  • DO NOT HOOK libxpc public stubs (_xpc_connection_cancel etc) — empirical absence of callers means hooks would not fire on AUA path.

Apparatus integrity (D37-F also non-destructive)

  • All four havoc binaries unchanged; SHA unchanged from D37-D/D37-E.
  • Plus successful pull of libdispatch.dylib (sha 35f8337ed319…ca8011816) and libdispatch-introspection.dylib (sha 38723f0175a0…69b6e822) from havoc DSC.
  • iosmux_inject sha unchanged (52df2cc6...4803).
  • iPhone (iosmux) connected (no DDI); CDS PIDs 1013+10797 stable.
  • Zero new DiagnosticReports.

Toolchain limit discovered

ipsw cannot extract libxpc.dylib from macOS Sequoia 26.x DSCs. Reproducible failure at failed to rebase dylib via cache slide info: invalid command block size in record at byte 0x1208. libdispatch + most others extract cleanly. For libxpc Ghidra disassembly future probes need either Apple's dsc_extractor (full Xcode), arm64 DSC, or Apple's open-source libxpc source. Documented for future probes.

Distillation status

D37-F raw notes (notes/d37f-cross-binary-trigger.md, ~480 lines) deleted at distillation time per feedback_notes_are_temporary_buffer. Cancel-callsite enumeration + Mercury all-deinits + libxpc internal symbols + AUA wrapper trace preserved at /home/op/dev/iosmux/symbols/d37f-*.txt (host-side, not in repo). DSC files at /home/op/dev/iosmux/dsc/ (~5.6 GB, host-side, not in repo). Verdict + new G-4 + anti-recommendations preserved in this section + the Q-D66-15 D37-F Resolution log entry.

D37-E update: dtrace empirical FALSIFIES D37-D's listener-deinit-IS-trigger hypothesis (with strong Heisenbug caveat); D37-D symbol mapping corrected; alternative trigger candidates identified (2026-05-02)

D37-E ran dtrace pid-provider on havoc during AUA invocation to empirically confirm/falsify §D-1 (D37-D's hypothesis that Mercury.SystemXPCListenerConnection.deinit at Mercury+0x4e930 fires upstream of HIT #18 libxpc cancel).

Result: §D-1 EMPIRICALLY FALSIFIED — but with strong caveat

Across 4 dtrace runs (3 of which reproduced "Recieved error from side channel peer" partial Path-A signature), Mercury+0x4e930 (listener.deinit) did NOT appear in any of:

  • 22 _xpc_connection_mach_event entries (HIT #18 surface — reason=0x2 with ZERO Swift app frames matches D34 reference exactly)
  • 1770 xpc_release entries (mostly XPC dictionary deserialize chains)
  • 2 xpc_connection_cancel entries (CoreAnalytics process-exit cleanup, unrelated)
  • 4 _xpc_connection_cancel entries (libxpc-internal, no app drivers)

ZERO occurrences of any of 0x4e930 / 0x4e960 / 0x4e9d0 / 0x4ea10 across 4 runs × ~30k stack frames each ≈ 120k+ frames. The hypothesised Mercury Swift deinit chain did not fire on any captured cancel-or-release path.

Strong Heisenbug caveat

dtrace -c with ustack(15-20) introduced the same timing perturbation as lldb (D35a) — none of the 6 runs reproduced FULL Path-A ("Successfully acquired" log absent in all dtrace runs; pre-dtrace baseline at 08:56:15 had it). 3/6 runs hit "Path-A-without-success-log" where side-channel peer is created and emits XPCError 1001 but the "Successfully acquired" log is suppressed. It remains possible that on full Path-A the listener-deinit cascade DOES fire but dtrace's overhead alters ARC release timing such that listener stays live until process exit.

Symbol mapping correction (vs D37-D)

D37-D's static brief had a copy/paste error. Empirical mapping per llvm-nm -arch x86_64 /home/op/dev/iosmux/binaries/Mercury:

File offset Mangled symbol Demangled
0x4e930 _$s7Mercury27SystemXPCListenerConnectionCfd listener deinit (body) — UNCHANGED
0x4e960 _$s7Mercury27SystemXPCListenerConnectionCfD listener __deallocating_deinit (D37-D wrongly said 0x4ea10)
0x4e9d0 _$s7Mercury23SystemXPCPeerConnectionCfd peer deinit (body) — UNCHANGED
0x4ea10 _$s7Mercury23SystemXPCPeerConnectionCfD peer __deallocating_deinit (D37-D wrongly assigned to listener)

The trigger candidate 0x4e930 listener.deinit is unchanged. The mismatched offset for __deallocating_deinit does not affect §D-1's trigger logic.

Methodological findings (apparatus knowledge)

These constrain ALL future dtrace work on this codebase:

  1. Apple dtrace v1.19 cannot probe Swift mangled symbols directly. The $ byte in _$s... is parsed as macro-variable lead, throwing Undefined macro variable in probe description. No documented escape exists.
  2. Pid-provider symbol enumerator filters out $-prefixed symbols even from :::entry glob — Mercury's ~93 enumerated probes are exclusively C-style helpers. Swift entry points are invisible to direct probing.
  3. AMFI/restricted-task blocks dtrace -p PID even with SIP disabled, on Apple-signed binaries (devicectl, CDS, etc.). The -c CMD form (fork+exec child) is the only working path.
  4. dtrace introduces a Heisenbug similar to lldb's — D35a finding confirmed: under dtrace -c, "Successfully acquired" log is suppressed in 100% of runs.
  5. Workaround used: probe libxpc C symbols (fully enumerable), capture ustack(15-20), back-resolve Mercury runtime addresses to file offsets via per-run slide arithmetic. Effective for indirect detection.

Alternative trigger candidates (to investigate next)

  • A: Listener.deinit fires only on full Path-A; dtrace suppresses Path-A. Hard to falsify without non-perturbing instrumentation.
  • B: HIT #18 trigger is UPSTREAM of Mercury Swift type destruction — possibly inside _dispatch_mach_send_drain which triggers kernel-side peer-cancel before any Swift destructor runs. The captured reason=0x2 MACH_EVENT enters libxpc with zero Swift frames in 100% of cases.
  • C: Peer cancel originates from CDS side first (D32 confirmed kernel-driven MACH_NOTIFY_NO_SENDERS); devicectl-side listener release would be CONSEQUENCE not cause. This contradicts D32's "trigger in devicectl" inference but the inference was indirect.

Implication for D38 implementation

D37-D hook target recommendation should NOT be relied upon as primary without further empirical validation. Either:

  • the trigger is upstream of Mercury Swift class destruction (alt B/C);
  • or the trigger fires only on full Path-A which dtrace cannot capture without altering it (alt A).

Discriminator design [CD+0x30000, CD+0x32000) could not be empirically tested since no listener-deinit was observed in any captured stack.

Successor research-probe options (per §G of D37-E)

  • Path-A reproduction without instrumentation — direct USB capture
  • CDS-side inject counter-instrumentation; observe whether listener-deinit fires by side-effect (peer-table walk in CDS-side instrument capturing whether NO_SENDERS arrives correlated with Path-A Successfully acquired log).
  • CDS-side counter-instrumentation in iosmux_inject.dylib — augment with mach-port-name tracking; on every accepted peer in CDS, log birth + cancel timestamps. Path-A-vs-B distinction empirical without touching devicectl. Heisenbug-immune (in-process logging, no debugger).
  • dtrace USDT-only / static probes — Apple's os_signpost surface; available without per-instruction overhead.
  • Ghidra cross-binary rerun — load Mercury + CoreDevice + CDU + libxpc into one Ghidra project, full xref graph from libxpc's _xpc_connection_remove_peer_impl upward. Pure static, no apparatus.

Apparatus integrity (D37-E also non-destructive)

  • All four havoc binaries unchanged (READ-ONLY analysis only); SHA-equal to host backups.
  • iosmux_inject sha unchanged (52df2cc6...4803).
  • iPhone (iosmux) connected (no DDI).
  • CDS PID 1013 stable (ELAPSED 19h24m at run end). MINOR drift: a SECOND CDS instance (PID 10797) was launchd-spawned during probe window — both run concurrently. Non-destructive.
  • Zero new DiagnosticReports.

Distillation status

D37-E raw notes (notes/d37e-dtrace-listener-trigger.md, ~407 lines) deleted at distillation time per feedback_notes_are_temporary_buffer. Per-run dtrace dumps preserved at /home/op/dev/iosmux/symbols/d37e-stack*.txt (host-side, not in repo). Verdict + symbol corrections + methodological findings + successor options preserved in this section + the Q-D66-15 D37-E Resolution log entry.

D37-D update: HIT #18 trigger LOCALISED — Mercury.SystemXPCListenerConnection.deinit at Mercury+0x4e930 releases listener xpc handle, libxpc tears down listener + accepted peer (NO_SENDERS cascade); cause-side hook target identified (2026-05-02)

D37-D continued the host-side static disasm (existing D36 Ghidra project). Goal: identify the upstream code site that drops the LAST strong reference to peer[1013] BEFORE the Swift DeviceUsageAssertion.deinit chain at HIT #23. Apparatus integrity verified non-destructive.

Three Swift holders of peer[1013] in devicectl (Q-D-1)

  1. Mercury.SystemXPCListenerConnection — anonymous listener allocated at CD+0x304a0+0x152 via Mercury.SystemXPCConnection.anonymousListenerConnection. Holds the accepted peer transitively via libxpc's listener peer-table (objc level, NOT Swift ARC).
  2. lVar17 AUA-completion context — Swift heap object allocated at CD+0x304a0+0x186, captures listener at offset +0x18 and nested closure context at offset +0x28. The setPeerConnectionHandler block + the Swift continuation that awaits forward<> both retain this context.
  3. CoreDevice.DeviceUsageAssertion._IndexBox<Mercury.SystemXPCPeerConnection> — boxed peer field inside DUA, populated only on success-resume. This is the LATE holder that releases at HIT #23 (t=111.269s).

Construction path (CD+0x304a0 = acquireDeviceUsageAssertion(...completion:))

+0x152: Mercury.SystemXPCConnection.anonymousListenerConnection  → SystemXPCListenerConnection
+0x163: objc_msgSend(... selector=5 ...)                         → xpc_endpoint_create_from_connection (Input.endpoint)
+0x186: _swift_allocObject() → lVar17                            → AUA-completion context (heap, ~0x40 bytes)
          lVar17+0x18 = listener (STRONG ref)
          lVar17+0x28 = forward<>'s closure context
+0x228: SystemXPCListenerConnection.setPeerConnectionHandler     → installs Swift closure on listener
+0x232: Mercury.XPCConnection.activate(listener)                 → xpc_connection_activate
+0x277: CoreDeviceUtilities.ActionDeclaration.forward<>(...)     → dispatches AUA action to CDS
+0x281: synchronous local cleanup (drops local retain ref)

After return only the listener-via-AUA-completion-context path persists. When CDS dials back, the peer-handler block constructs a SystemXPCPeerConnection via Mercury.SystemXPCConnection.unsafePeer(from:) (Mercury+0x4d070) and captures it into the same closure context.

THE TRIGGER for HIT #18 (Q-D-3)

Mercury.SystemXPCListenerConnection.deinit at Mercury+0x4e930:

SystemXPCListenerConnection *deinit(this) {
  _swift_bridgeObjectRelease();    // empty array storage at +0x10
  _swift_unknownObjectRelease();   // *** xpc_release on listener's OS_xpc_connection ***
  FUN_0003b090();                  // helper (objc_release-like)
  return this;
}

The _swift_unknownObjectRelease(listener_xpc) call drops devicectl's last strong ref to the underlying listener OS_xpc_connection. libxpc internally walks the listener's peer table and propagates cancellation to each accepted peer — D32 observed this at t+126ms via _xpc_connection_remove_peer_impl. This IS what fires HIT #18 at t=107.510 (D34's _xpc_connection_mach_event+0x310, ZERO Swift app frames is consistent with libxpc internal listener-tear-down processing).

The 3.7-second gap between HIT #18 and HIT #23 is the gap between listener-wrapper release (driven by Swift Concurrency continuation completion in CD+0x304a0/CD+0x317a0 family) and DUA._IndexBox release (driven by command outcome scope ending at devicectl+0x16970).

Why static cannot pinpoint the EXACT instruction (Q-D-4 + §E gap 2)

The lVar17 AUA-completion context is released by Swift Concurrency runtime — _swift_continuation_init / _swift_continuation_resume / task-allocator destruction sequences live in libswift_Concurrency.dylib, not in CoreDevice's binary. There is no explicit xpc_connection_cancel(listener) call in CoreDevice or CoreDeviceUtilities for the AUA flow. Release is purely ARC-driven by the continuation runtime. Per ADR-0006 we don't speculate about Swift runtime internals.

Hook target (Q-D-5 / §D-1) — primary recommendation

Hook: prologue-patch Mercury.SystemXPCListenerConnection.deinit at Mercury+0x4e930 in devicectl via DYLD_INSERT_LIBRARIES (D33 P3 confirmed feasible).

Mechanism: same gambit_install_hook pattern from inject/iosmux_gambit.m (5-byte JMP rel32 to trampoline). Trampoline:

  • Stack-walk filter: any return PC in [CD+0x30000, CD+0x32000) (the AUA async wrapper family — 0x304a0 / 0x31750 / 0x31c00 / 0x317a0 / 0x31a50 / 0x31ab0) → AUA-context, suppress release.
  • Skip the _swift_unknownObjectRelease(listener_xpc) call inside the deinit body. Optionally xpc_retain(listener_xpc) upfront to keep libxpc bookkeeping consistent.
  • For non-matching listeners: tail-call original deinit prologue.

Confidence basis:

Aspect Verdict Confidence
Listener-deinit IS what fires before HIT #18 HIGH Mercury+0x4e930 body explicitly does _swift_unknownObjectRelease(listener_xpc) — direct trigger of libxpc listener-tear-down + peer NO_SENDERS cascade
HIT #18 IS triggered by listener tear-down (vs. some other release) MEDIUM-HIGH D32 observed _xpc_connection_remove_peer_impl at t+126ms (listener peer-table walk); D34 HIT #18 frame _xpc_connection_mach_event+0x310 has ZERO Swift app frames — consistent with listener-side processing not direct Swift-deinit
Hook mechanically feasible HIGH NAMED Swift symbol, dlsym-resolvable, same proven pattern as D23
AUA-listener discrimination via stack-walk works HIGH Swift continuations preserve return-PC frames pointing into CD+0x30... async wrapper
Suppressing release does not deadlock continuation MEDIUM Continuation completes via action's main XPC reply independent of listener lifecycle

Side effects of the hook

  • Resource leak: anonymous listener + underlying OS_xpc_connection (one mach port + bookkeeping) leaks per AUA invocation, bounded to process lifetime. devicectl is short-lived CLI per command — leak is bounded. Negligible.
  • CDS-side: CDS holds OS_xpc_remote_connection to devicectl's listener. With listener kept alive, CDS's connection stays alive until CDS scope releases it naturally. NO_SENDERS still fires EVENTUALLY but AFTER Path-A's success-resume completed — Path-A wins deterministically.
  • No cross-action interference: each AUA invocation has its own listener; non-AUA actions don't share it.

Anti-recommendations (do NOT hook these)

  • Mercury.SystemXPCPeerConnection.deinit (Mercury+0x4e9d0) — fires at HIT #23, LATE.
  • CoreDevice.DeviceUsageAssertion.deinit (CD+0xd3060) — HIT #23, LATE.
  • Mercury.XPCSideChannel.deinit (Mercury+0x1c450) — wrong type, not on AUA peer[1013] critical path.
  • _Continuation.resume(throwing:) / xpcError.getter / CoreDeviceError.init — D37-C deprecated as denial-masking.

Static analysis localised the TYPE of release (listener wrapper deinit) and the UPSTREAM scope (AUA-completion context release in CoreDevice's async path). It cannot verify the listener-deinit-IS-trigger claim 100% — alternative hypothesis would be a direct xpc_connection_cancel(listener_xpc) somewhere in continuation completion code that static missed. Cross-binary xrefs are invisible (each Ghidra program standalone), and Swift Concurrency runtime is opaque.

Recommended dtrace probe for empirical confirmation (separate agent dispatch): pid-provider on Mercury+0x4e930 entry + libxpc:_xpc_connection_mach_event:entry correlated with timestamps, ustack(20). If Mercury+0x4e930 fires at ~t=107.5s just before HIT

18 → §D-1 hypothesis confirmed. If it doesn't fire at that time →

§D-1 falsified, alternative trigger to investigate.

dtrace's lower per-probe overhead vs lldb (no SIGSTOP) avoids the D35a Heisenbug.

Apparatus integrity (D37-D also non-destructive)

  • All four havoc binaries unchanged (READ-ONLY analysis only).
  • Same checksums as D36/D37-C: devicectl 4fede2dd…bf6c, CoreDevice bea205e2…0495, CoreDeviceUtilities b7ce01c4…7204, Mercury 753f919e…cdbb. iosmux_inject sha unchanged (52df2cc6…4803).
  • iPhone (iosmux) connected (no DDI). CDS PID 1013 stable.
  • Zero new diagnostic reports.

Distillation status

D37-D raw notes (notes/d37d-upstream-peer-release.md, ~530 lines) deleted at distillation time per feedback_notes_are_temporary_buffer. Per-binary ownership-trace dumps preserved at /home/op/dev/iosmux/symbols/d-ownership-trace-*.txt and d-ownership-followup*.txt (host-side, not in repo). Empirical findings + ownership graph + hook target preserved in this section + the Q-D66-15 D37-D Resolution log entry in docs/plans/d66-research-questions.md.

D37-C update: hook candidate #1 (resume(throwing:)resume(returning:) redirect) is mechanically feasible for Void Output BUT semantically masking a denial; all "fake-success" hooks (#1/#2/#3) deprecated (2026-05-01)

D37-C focused single-question probe via host-side static disasm (Ghidra on already-imported D36 project). Determined the Swift ABI for _Continuation.resume(returning:) at CDU+0x60160 to assess viability of hook candidate #1 from D36 (redirect Path-B's resume(throwing:) to resume(returning:) with Void success).

Calling convention (Q-C1)

resume(returning: A) at CDU+0x60160:

  • RSI = self (the _Continuation instance)
  • R13 = pointer to indirect A-buffer (the value to return)
  • A's type metadata (VWT) read from self+0x10
  • Dynamic stack via __chkstk_darwin (variable, depends on size of A)

Body: read A's VWT → call VWT slot 0x10 (initializeWithCopy) to copy A from R13-pointed buffer into a Result-shaped local → set enum tag=0 (.success) → call shared tail FUN_00060680 → destroy temporaries.

Comparison to resume(throwing:) (Q-C2)

resume(throwing: Error) at CDU+0x5fb70 has DIFFERENT shape:

  • RDI = error existential (NOT in RSI)
  • No A-buffer touched; tag=1 (.failure)
  • Adds _swift_errorRetain on the error before storing
  • Same shared tail FUN_00060680 afterward

The two are NOT trivially interchangeable — register convention, buffer handling, and refcount semantics all differ.

Void-Output callsite reality (Q-C3)

ZERO direct callers of resume(returning:) inside CoreDeviceUtilities. Void-Output forward<> specializations use (Error?) -> () trampolines (FUN_00055510 = (*(R13+0x10))(0)) that bypass resume(returning:) entirely — they signal completion through a different vtable path. Real callers live outside CDU (likely in CDS, CoreDevice, or Mercury, where the AUA action's witness table holds its forward(continuingUsing:) implementation).

Hook viability verdict (Q-C4 / §E)

Aspect Verdict Confidence
Mechanical ABI feasibility (forge call) YES, ~10 lines of asm trampoline HIGH
Void-Output value fabrication No fabrication needed (size 0; () has no-op VWT) HIGH
Type-metadata reachability OK (already bound in self+0x10) MEDIUM-HIGH
Downstream awaiter consistency UNKNOWN — depends on action contract LOW
Overall: should we land hook #1? NO HIGH

Mechanical forgery is empirically possible — a ~10-line asm hook can intercept resume(throwing:) at CDU+0x5fb70, filter by error type + caller context, release the error existential via _swift_errorRelease, and jump into resume(returning:) past the chkstk prologue. For Void Output specifically this is even simpler (no value to construct).

But hook #1 masks a CDS-authoritative denial signal. If we forge success at the continuation level, devicectl's awaiter proceeds as if authorization succeeded — then fails unpredictably at the next async step (no real session, peer invalidation handled with stale state, no XPC payload carrying session data).

This finding extends to hooks #2 (xpcError.getter) and #3 (CoreDeviceError.init) by analogy: all three operate at the "error-suppression" layer and would mask the same denial signal at different points along the chain. The denial is what it is — synthesizing success above it doesn't make AUA actually succeed.

Implication for D36 hook candidate ranking

Of the 5 D36 candidates:

  • #1 (resume(throwing:) redirect): DEPRECATED per this verdict
  • #2 (xpcError.getter filter): same semantic flaw — DEPRECATED
  • #3 (CoreDeviceError.init filter): same semantic flaw — DEPRECATED
  • #4 (signalCompletion gating): different layer — affects which path WINS the race rather than masking the loser. Still viable. Static cannot classify which of the 5 callers is Path-A vs Path-B; runtime probe (dtrace) needed before this can be implemented.
  • #5 (_completionGuard pre-signal from inject): race-timing approach. Still viable but requires Swift class-metadata field-offset walk at runtime. Doesn't prevent Path-B from also calling signalCompletion later.

The remaining viable directions are race-timing (#4/#5) or upstream prevention of peer[1013] release in devicectl (currently un-localised; D34 saw a Swift DeviceUsageAssertion.deinit chain at HIT #23 firing 3.7 seconds AFTER HIT #18 libxpc cancel, so HIT #18's trigger remains upstream of any code site we have mapped).

Apparatus integrity (D37-C also non-destructive)

  • Apple binaries on havoc unchanged (READ-ONLY analysis only).
  • Same checksums as D36: devicectl 4fede2dd…bf6c, CoreDevice bea205e2…0495, CoreDeviceUtilities b7ce01c4…7204, Mercury 753f919e…cdbb. iosmux_inject sha unchanged (52df2cc6…4803).
  • iPhone (iosmux) connected (no DDI). CDS PID 1013 stable.
  • Zero new diagnostic reports.

Distillation status

D37-C raw notes (notes/d37c-continuation-abi.md, ~516 lines) deleted at distillation time per feedback_notes_are_temporary_buffer. Decompile dumps preserved at /home/op/dev/iosmux/symbols/c-continuation-resume-abi.txt (~3079 lines, host-side, not in repo). Empirical findings + ABI spec + 5 candidate ranking update preserved in this section + the Q-D66-15 D37-C Resolution log entry in docs/plans/d66-research-questions.md.

D36 update: host-side static disasm — full Path-A/Path-B static map; convergence is DevicectlExecutor._completionGuard semaphore; XPCError→CoreDeviceError(3) is hardcoded (2026-05-01)

D36 chose Option 4 (host-side static disasm with Ghidra) over the 3 lldb/dtrace alternatives because of the D35a Heisenbug. Four Apple binaries pulled READ-ONLY from havoc to host (/home/op/dev/iosmux/binaries/), analysed with ghidra-analyzeHeadless, llvm-objdump, swift-demangle, and radare2. Apparatus integrity verified: zero modifications to havoc binaries; all four sha256 matched expected; iPhone (iosmux) connected (no DDI); CDS PID 1013 stable.

Three unnamed devicectl symbols — IDENTIFIED

devicectl's symbol table is heavily STAB-stripped (~4000 entries are <redacted function N>); three target offsets had no Swift mangled names. Ghidra decompile + r2 strings + relocation evidence resolved them to:

file off inferred Swift identity role confidence
0x100016970 CoreDeviceCLISupport.AnyDevicectlCommand extension method CLI command result printer; calls Logger._info, references string "Current device information:" medium (string + xref evidence)
0x100092c60 command error finalizer builds CoreDeviceClientJSONSupport.EncodableCommandOutput, dispatches CommandInfo.Outcome.{success,failed}, calls ArgumentParserInternal.ExitCode.exit() high (relocation evidence)
0x100095660 devicectl.DevicectlExecutor.signalCompletion does objc_retain(_completionGuard) → OS_dispatch_semaphore::wait → objc_release → swift_unknownObjectRetain(_timerSource) → OS_dispatch_source::cancel → swift_unknownObjectRelease high (Ghidra type-resolution: OS_dispatch_semaphore and OS_dispatch_source from Swift type metadata)

Path-A and Path-B converge on signalCompletion

Both Path-A (success → side-channel cancel → 1001 → errorCode 3) and Path-B (success → errorCode 3 directly) terminate at the SAME finalizer:

devicectl.DevicectlExecutor.signalCompletion (devicectl+0x95660) → dispatch_semaphore_wait(self._completionGuard)dispatch_source_cancel(self._timerSource)

5 callers of FUN_100095660 found (axt): 0x100092c60, 0x100094480, 0x100094cd0, 0x100096500, 0x100096ab0. Each is a different action-completion code path. Static cannot classify which is Path-A vs Path-B at the moment of signaling — runtime probe (dtrace pid$target:devicectl::FUN_100095660:entry with ustack) needed if classification matters.

Empirical chain confirmed end-to-end (Path-A)

Mercury.XPCError(code:1001, "...invalidated", peer[1013])
  → Mercury.XPCError.normalized(as: CoreDeviceError.Type)   (Mercury+0x52d50)
    → static (extension in CoreDevice):Swift.Error<...>.xpcError.getter (CDU+0xc2940)
      → CoreDeviceError._init(code: 3, userInfo: ...)         (CDU+0xbabf0)
        → _Continuation.resume(throwing:)                     (CDU+0x5fb70)
          → ActionConnection trampoline `(*[r13+0x10])(0)`    (CDU+0x55510)
            → DevicectlExecutor.signalCompletion              (devicectl+0x95660)
              → dispatch_semaphore_wait(_completionGuard)
              → dispatch_source_cancel(_timerSource)

The mov edi, 3 at CDU+0xc296b proves the XPCError → CoreDeviceError conversion is hardcoded at the layer of Swift.Error<...>.xpcError.getter: it ALWAYS returns CoreDeviceError(code: 3) regardless of the original XPCError's code (1001 etc). The 1001 only survives in userInfo.NSLocalizedDescription.

_timerSource cancel — D35a "dispatch_channel_cancel" was lldb symbol-aliasing

D35a's lldb stack frame "dispatch_channel_cancel ONCE at t=3.958" was actually OS_dispatch_source::cancel(_timerSource) — Ghidra resolves the call at devicectl+0x100095660+0x70 directly to OS_dispatch_source::cancel. lldb's symbol-name resolution for libdispatch internal helpers may have picked up dispatch_channel_cancel as a related symbol. Semantic equivalent: some dispatch object is being cancelled. The actual target is the executor's deadline timer.

_Continuation does NOT enforce resume-once visibly

CDU+0x5fb70 (_Continuation.resume(throwing:)) decompile shows no atomic check, no flag, no early-return — just unconditionally builds Result.failure(...) and calls a vtable method. Resume-once protection comes from EITHER:

  • Bottom layer: Swift Concurrency runtime's continuation slot atomic flag in libswift_Concurrency.dylib (double-resume causes SWIFT TASK CONTINUATION MISUSE runtime abort)
  • Middle layer: DevicectlExecutor._completionGuard semaphore — the FIRST caller of signalCompletion's dispatch_semaphore_wait proceeds; subsequent callers block (semaphore created with count=1, used as mutex)

The empirical fact that we don't see Swift-runtime double-resume crashes means EITHER each path has its own continuation OR the _completionGuard mutex serialises them above the continuation layer.

Path-B trampoline at CDU+0x55510 — forward family closure

Q3 confirmed CDU+0x55510 is a 35-byte trampoline (*[r13+0x10])(0). 3 callers, all in $forward (the named ActionDeclaration.forward<Input == ()>(toDeviceIdentifiedBy:...completion:) extension at CDU+0x54c90). It is the Swift compiler-emitted closure trampoline for forward<>(...) async throws, not a dedicated drain pump. The "drain pump" framing from D31 was runtime-attribution from _dispatch_lane_serial_drain stack frames; statically the function is part of the action-forwarding mechanism.

Hook candidates from static map (NO implementation)

Ranked by confidence + cleanness:

  1. _Continuation.resume(throwing:) at CDU+0x5fb70 — NAMED Swift symbol, dlsym-resolvable. Choke point for ALL action errors. Hook would inspect error in $rsi, filter by errorCode==3 + AUA context, redirect to _Continuation.resume(returning:) at CDU+0x60160 instead. Open: what value to pass to resume(returning:) — for AUA Output is Void, so it might be empty/null/zero. Needs verification.
  2. Swift.Error<...>.xpcError.getter at CDU+0xc2940 — NAMED, called from Mercury's normalization path. Hook would short-circuit when the underlying XPCError code ∈ {1001} AND caller is AUA-related. Returns alternative success-shaped value. Higher complexity (need to fabricate alternative return value of generic type A: _Error).
  3. CoreDeviceError._init(code:userInfo:) at CDU+0xbabf0 — NAMED Swift initializer. Hook filters code == 3 AND AUA stack-context, rewrites to code == 0 or skips construction. Concern: error is constructed at this point but resume-throwing is not yet called; unclear if downstream code tolerates "error with code 0" vs "no error at all".
  4. DevicectlExecutor.signalCompletion at devicectl+0x95660 — convergence point. Hook would gate which path proceeds. Static cannot classify the 5 callers as Path-A vs Path-B without runtime probe; hook utility unclear without that.
  5. DevicectlExecutor._completionGuard semaphore (pre-signal from inject) — would require Swift class metadata walk to find field offset within DevicectlExecutor, then dispatch_semaphore_signal from inject-side. Allows pre-allowing one path; doesn't prevent the other from also calling signalCompletion later.

All 5 candidates are in CoreDeviceUtilities or devicectl; both load into the devicectl process. D33 already proved DYLD_INSERT_LIBRARIES injection feasible; standard inject mechanism applies.

What static analysis cannot resolve

  • Whether signalCompletion's 5 callers map to Path-A vs Path-B (needs runtime classification).
  • Field offset of _completionGuard within DevicectlExecutor (Swift field-descriptor table walk needed at runtime).
  • Whether resume-once is enforced AT _Continuation's level via an external state flag, or only at libswift_Concurrency.dylib level.
  • Q2 (peer release trigger): static adds NO new data. D32 verdict stands — kernel-driven via MACH_NOTIFY_NO_SENDERS, no application code site. Mercury+0x4e9d0 SystemXPCPeerConnection.deinit fires AFTER libxpc release and cannot prevent it.

Apparatus integrity (D36 also non-destructive)

  • Apple binaries on havoc unchanged (READ-ONLY pulls only): devicectl sha 4fede2dd…bf6c, CoreDevice sha bea205e2…0495, CoreDeviceUtilities sha b7ce01c4…7204, Mercury sha 753f919e…cdbb.
  • iosmux_inject.dylib sha unchanged (52df2cc6…4803).
  • iPhone (iosmux) connected (no DDI).
  • CDS PID stable at 1013.
  • Zero new diagnostic reports.

Distillation status

D36 raw notes (notes/d36-host-disasm.md, ~698 lines) deleted at distillation time per feedback_notes_are_temporary_buffer. Per-question text outputs preserved at /home/op/dev/iosmux/symbols/ (host-side, not in repo). Ghidra project at /home/op/dev/iosmux/ghidra/D36/ — re-runnable.

See also