arm64e, SPTM kernelcaches — macOS 26 / Darwin 25. Using only ipsw, the
blacktop signature set, and the slides already in the panic header.
On recent Apple-silicon Macs the released kernelcache strips core-XNU
symbols — kext C++ symbols survive, the kernel proper does not. So the raw
lr: backtrace in a .panic file resolves to meaningless adjacent symbols: a
"symbol salad" with GPU, video-codec, and Neural-Engine functions sharing one
impossible call stack. This is how to get the real names back, and the one
non-obvious trick that makes it work:
Pick the KASLR slide by panic-machinery coherence, not by arithmetic.
The payoff — turning this:
7 __ZL22GenerateGrainLag2_neonILb0EEvP18AV1FilmGrainParams…+1920 ← "panic caller"?!
11 __ZN22ACMRMEnvironmentConfig20configureEnvironmentE…+616 ← faulting pc?!
13 __ZN11ANEHWDevice23ReadPerformanceCountersE…+236
into this:
8 handle_kernel_tag_check_fault+1440
10 fleh_synchronous+72
11 cfil_info_action_timed_out+16 ← FAULTING pc
12 cfil_info_log+152
13 soflow_gc_expire+232
14 user_take_ast+948
A released kernelcache is an MH_FILESET. Run nm on it and you get hundreds of
thousands of symbols — but they're overwhelmingly kext symbols (mangled C++
like __ZN24AGXFirmwareResourceStack…). The core kernel's functions (panic,
sleh_synchronous, zalloc, the networking stack, …) have no symbol table
entries. lldb confirms it: load the cache and it spits
invalid string table offset for the kernel's stripped nlist.
So when you resolve a backtrace address that lands in core XNU, the nearest preceding symbol is whatever kext function happens to sit before it in memory. You get plausible-looking names that are completely wrong. The dead giveaway: unrelated subsystems (GPU + AV1 + ANE) in a single stack. A real kernel stack stays within a subsystem or threads through core XNU.
A .panic file (bug_type 210) is one JSON metadata line then a JSON body; the
human-readable header + backtrace live in body["panicString"]. The pieces you
need:
- Backtrace — the
lr:list underPanicked thread:. - Fileset Kernelcache UUID — identifies exactly which on-disk kernelcache ran.
- Three KASLR slides —
KernelCache slide,Kernel slide,Kernel text exec slide. On SPTM systems these differ, because the kernel, its text-exec segment, and the whole collection are slid independently. - Kernel version —
Darwin Kernel Version 25.4.0→ signature set25.4.
The booted kernelcache is in Preboot (the .kc files under
/System/Library/KernelCollections are not the fused, booted artifact). There
are usually two; only one matches the panic's Fileset UUID.
for kc in /System/Volumes/Preboot/*/boot/*/System/Library/Caches/com.apple.kernelcaches/kernelcache; do
cp "$kc" /tmp/kc.im4p
ipsw kernel dec /tmp/kc.im4p # -> /tmp/kc.im4p.decompressed
ipsw macho info /tmp/kc.im4p.decompressed | grep LC_UUID # match against the panic
doneipsw kernel symbolicate pattern-matches the blacktop/symbolicator
signature set against the cache and recovers the kernel symbol names — ~92% on a
current build:
git clone --depth 1 https://github.com/blacktop/symbolicator ~/.cache/symbolicator
ipsw kernel symbolicate --signatures ~/.cache/symbolicator/kernel/25.4 \
--flat -o /tmp /tmp/kc.im4p.decompressed # -> /tmp/…decompressed.symsMerge those with the kext symbols already in the cache (nm -arch arm64e -n) for
a complete address→name table.
This is the part that cost me an hour. The intuitive move is to unslide each
backtrace address with Kernel text exec base − Kernel text exec slide. It
gives garbage. Apple references the reported "text exec slide" to the __TEXT
(read-only) segment, not __TEXT_EXEC, so the math lands you in the wrong
place and you get a fresh symbol salad — twice, if you're stubborn.
The robust fix: you have three candidate slides and a dense symbol table. Resolve
the backtrace at each reported slide, and keep the one whose inner frames
land on the panic/exception machinery — panic, panic_trap_to_debugger,
sleh_synchronous, fleh_synchronous, handle_kernel_tag_check_fault, … A
correct slide lights up many of these; a wrong slide lights up none. (Don't score
by "small offset to nearest symbol" — with a 200k-symbol table every slide
lands close to something. Score by hitting the specific functions that can only
appear in a panic stack.)
On the example below, the winner was the KernelCache slide (0x46e74000),
not the text-exec slide the header seems to point you at.
The faulting frames are the contiguous run below the last machinery frame — the code that actually took the fault and its callers. Everything above is the panic/exception plumbing.
The panic that started this: Kernel tag check fault — Apple-silicon hardware
memory-tagging (pointer tag 0xf3 vs the freed-and-retagged slot's 0xfe)
catching a use-after-free in XNU's socket content-filter (CFIL) flow
garbage collector:
user_take_ast
→ soflow_gc_expire (socket-flow GC on return to userspace)
→ cfil_info_log
→ cfil_info_action_timed_out ← freed cfil_info dereferenced
→ fleh_synchronous → sleh_synchronous → handle_kernel_tag_check_fault → panic
Trigger: an active NEFilterDataProvider content filter (here, Little Snitch
Mini) makes XNU track every socket flow through CFIL; sustained socket churn over
a ~6.7-day uptime eventually hit a latent UAF in content_filter.c. An Apple
kernel bug, exercised — not caused — by the content filter. macOS 26.4.1
(25E253), Mac17,6.
A tag-check fault, incidentally, is the hardware catching a software
memory-safety bug, not bad RAM — and panicString: Sleep 0 / Wake 0 rules out
any wake-transition theory before you even reach for pmset.
kernel-panic-symbolicate.py (attached) automates all of the above:
kernel-panic-symbolicate.py --latest # newest panic in DiagnosticReports
kernel-panic-symbolicate.py /path/to.panic --jsonIt matches the cache by Fileset UUID, decompresses, recovers XNU symbols,
auto-selects the coherent slide, and prints the annotated stack. Artifacts cache
under ~/.cache/kernel-panic-sym/<uuid>/ (first run ~25s, repeats ~0.1s).
Requirements: ipsw (brew install ipsw), Xcode CLT (nm), git.
All the heavy lifting is blacktop's — ipsw for the kernelcache
plumbing and the symbolicator signature set for XNU symbol recovery. The
only thing here is the glue and the slide-by-coherence heuristic.