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:
- Path-A reproduction in current D23 v2 apparatus — rare (0/30 historical), unlikely without apparatus changes
- devicectl-side counter-instrumentation (D37-H?) — separate
iosmux_devicectl_inject.dylibper D33 P3 feasibility, hooksMercury.SystemXPCListenerConnection.deinitat 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.cancelimpl (dispatch thunk) - Mercury+0x3d239 —
RemoteXPCConnection.cancelimpl - 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_3b090helper 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_canceletc) — 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(sha35f8337ed319…ca8011816) andlibdispatch-introspection.dylib(sha38723f0175a0…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_evententries (HIT #18 surface —reason=0x2with ZERO Swift app frames matches D34 reference exactly) - 1770
xpc_releaseentries (mostly XPC dictionary deserialize chains) - 2
xpc_connection_cancelentries (CoreAnalytics process-exit cleanup, unrelated) - 4
_xpc_connection_cancelentries (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:
- Apple dtrace v1.19 cannot probe Swift mangled symbols
directly. The
$byte in_$s...is parsed as macro-variable lead, throwingUndefined macro variable in probe description. No documented escape exists. - Pid-provider symbol enumerator filters out
$-prefixed symbols even from:::entryglob — Mercury's ~93 enumerated probes are exclusively C-style helpers. Swift entry points are invisible to direct probing. - AMFI/restricted-task blocks
dtrace -p PIDeven with SIP disabled, on Apple-signed binaries (devicectl, CDS, etc.). The-c CMDform (fork+exec child) is the only working path. - dtrace introduces a Heisenbug similar to lldb's — D35a finding
confirmed: under dtrace
-c, "Successfully acquired" log is suppressed in 100% of runs. - 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_drainwhich triggers kernel-side peer-cancel before any Swift destructor runs. The capturedreason=0x2MACH_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 acquiredlog). - 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_signpostsurface; 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_implupward. 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)¶
Mercury.SystemXPCListenerConnection— anonymous listener allocated atCD+0x304a0+0x152viaMercury.SystemXPCConnection.anonymousListenerConnection. Holds the accepted peer transitively via libxpc's listener peer-table (objc level, NOT Swift ARC).lVar17AUA-completion context — Swift heap object allocated atCD+0x304a0+0x186, captures listener at offset +0x18 and nested closure context at offset +0x28. ThesetPeerConnectionHandlerblock + the Swift continuation that awaitsforward<>both retain this context.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. Optionallyxpc_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_connectionto 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.
Caveats and recommended empirical follow-up (§E)¶
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, CoreDevicebea205e2…0495, CoreDeviceUtilitiesb7ce01c4…7204, Mercury753f919e…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_Continuationinstance)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_errorRetainon the error before storing - Same shared tail
FUN_00060680afterward
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 (
_completionGuardpre-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, CoreDevicebea205e2…0495, CoreDeviceUtilitiesb7ce01c4…7204, Mercury753f919e…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 causesSWIFT TASK CONTINUATION MISUSEruntime abort) - Middle layer:
DevicectlExecutor._completionGuardsemaphore — the FIRST caller of signalCompletion'sdispatch_semaphore_waitproceeds; 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:
_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 toresume(returning:)— for AUA Output isVoid, so it might be empty/null/zero. Needs verification.Swift.Error<...>.xpcError.getterat 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 typeA: _Error).CoreDeviceError._init(code:userInfo:)at CDU+0xbabf0 — NAMED Swift initializer. Hook filterscode == 3AND AUA stack-context, rewrites tocode == 0or 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".DevicectlExecutor.signalCompletionat 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.DevicectlExecutor._completionGuardsemaphore (pre-signal from inject) — would require Swift class metadata walk to find field offset withinDevicectlExecutor, thendispatch_semaphore_signalfrom inject-side. Allows pre-allowing one path; doesn't prevent the other from also callingsignalCompletionlater.
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
_completionGuardwithinDevicectlExecutor(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+0x4e9d0SystemXPCPeerConnection.deinitfires 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 shabea205e2…0495, CoreDeviceUtilities shab7ce01c4…7204, Mercury sha753f919e…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¶
aua-side-channel-mechanism.mdaua-history-d38-d40.md— D38 falsifications based on D37-D/F's recommendations.aua-history-d24-d35.md— empirical localisation that motivated the D36 method pivot.