sysctl_intrnames traverses every CPU's interrupt-handler lists unsynchronized β use-after-free read / kernel heap disclosure via hw.intrnames
| Field | Value |
|---|---|
| ID | DF-2775 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:H |
| CWE | CWE-362 / CWE-416 |
| File | sys/kern/kern_intr.c |
| Lines | 1100-1116 (free side :448/:485-487) |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The hw.intrnames sysctl handler walks the per-CPU i_reclist chains of ALL cpus from the reader's own CPU with no critical section, no migration, and no lock. unregister_int unlinks a rec under a critical section on the target CPU only, then frees rec/rec->name with kfree after crit_exit. A reader that has already loaded a rec pointer but is timesliced or preempted (an ordinary user syscall thread) will, on resume, dereference freed memory: rec->next and rec->name passed as %s into ksnprintf. If the freed rec chunk is reclaimed by attacker-influenced kernel allocations, rec->name becomes an attacker-influenced pointer and ksnprintf emits a NUL-terminated string read from an arbitrary kernel address to the unprivileged reader β a kernel memory disclosure; otherwise the walk corrupts on garbage and panics.
Threat model & preconditions
Any local user can read hw.intrnames (CTLFLAG_RD, no privilege; systat -i uses it). The freeing side needs a concurrent driver teardown: kldunload/drvctl detach (root), USB/PCIe hot-unplug (physical attacker or VM host action), or the shutdown/reboot detach storm racing a reader β repeatable arbitrary-address string leak for a physical attacker.
Recommended fix
Migrate to each CPU and take a critical section around the list traversal (matching the register/unregister protocol), or maintain a stable snapshot under a spin/token β minimal patch in the row above.
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_intr.c (GLM 5.3); free-side needs root/hardware trigger on the stock guest, honest not-live-verified.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2775 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 5.6 KB | β raw | |
| run.log | β | 3.7 KB | view raw | |
| panic.txt | β | 562 B | view raw | |
| env.txt | β | 263 B | view raw | |
| fix.diff | β | 5.3 KB | view raw | |
| build.log.gz | β | 191.3 KB | β download | |
| src/df_harness.c | β | 22.3 KB | view raw | |
| src/df2775_reader.c | β | 3.8 KB | view raw | |
| src/pin_drop.c | β | 1.1 KB | view raw | |
| src/chk.c | β | 628 B | view raw | |
| logs/boot.after4thpanic.log | β | 13.0 KB | view raw | |
| logs/hits.log | β | 23 B | view raw | |
| logs/hits_fix.log | β | 2.2 KB | view raw | |
| logs/hits_fix0.log | β | 150 B | view raw | |
| logs/reader_p2.log | β | 49 B | view raw | |
| logs/reader_p3.log | β | 22 B | view raw | |
| logs/reader_p5.log | β | 55 B | view raw | |
| logs/reader_fix.log | β | 2.2 KB | view raw | |
| logs/reader_fix0.log | β | 182 B | view raw | |
| logs/dmesg.fixedkernel.log | β | 14.9 KB | view raw | |
| logs/df2776_wild_panic.txt | β | 466 B | view raw | |
| verdict.json | β | 5.1 KB | view raw |
DF-2775 evidence pack β sysctl_intrnames unsynchronized walk (UAF read / heap disclosure / panic)
Finding: sys/kern/kern_intr.c:1100-1116 β sysctl_intrnames traverses every
CPU's i_reclist chains with no synchronization (no crit section, no token,
reader runs on any CPU) while unregister_int unlinks (under a critical
section on the owning CPU only, kern_intr.c:438-448) and then frees
rec->name + rec (kern_intr.c:486-487). A reader that has loaded a rec
pointer can dereference rec->name after the free.
hw.intrnames is world-readable (CTLFLAG_RD), so the reader side is any
unprivileged user (systat/vmstat use it).
What is in this pack
src/df_harness.cβ kernel harness compiled into the test kernel:- exact copy of the stock walk with a stall point between rec load and
rec->nameuse (deterministic primitive proof), - churn thread on cpu1 driving register/unregister + an interposed
unregister (unlink -> kfree -> immediate same-cpu kmalloc reclaim of the
freed intrec, filled with a chosen
rec->name: harness token / kernelversionstring / arbitrary address), - fixed-walk variant (migrate+crit snapshot) for the A/B illustration.
src/df2775_reader.cβ UNPRIVILEGED userland reader hammeringsysctl hw.intrnames, scanning every entry for anomaly markers; writes O_SYNC logs so evidence survives crashes.src/pin_drop.cβ root helper: pin to a cpu, drop to uid 1001, exec the reader (USCHED_SET_CPU needs privilege; the mask survives the uid drop).src/chk.cβ output-correctness checker for the fixed sysctl.build.log.gzβ full untrimmed kernel build log (stock K1c build).run.logβ the decisive outputs (rigged token/version capture, organic panic signature x4, fix A/B).panic.txt,logs/boot.after4thpanic.logβ organic panic from the unprivileged reader (Stopped at strlenon the reader's cpu).logs/hits*.log,logs/reader_*.logβ reader runs (stock + fixed).env.txtβ guest environment.fix.diffβ canonical fix (pristine -> fixed), applies to stocksys/kern/kern_intr.c(covers DF-2775/2776/2777).
Build (on the guest)
scp src/df_harness.c root@guest:/usr/src/sys/kern/ echo 'kern/df_harness.c standard' >> /usr/src/sys/conf/files cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC install kernel.stripped as /boot/kernel/kernel, reboot cc -O2 -o df2775_reader.bin src/df2775_reader.c cc -O2 -o pin_drop src/pin_drop.c
Run
# deterministic primitive (groomed token, then kernel version string): sysctl dfh.mode2775=2 dfh.target2775=0 dfh.run2775=2 sysctl dfh.target2775=1 dfh.run2775=2 # organic race: unpriv reader on cpu0, churn on cpu1 (EXPECT PANIC): nohup /root/pin_drop 0 1001 ./df2775_reader.bin 90 0 /home/maxx/logs/hits.log & sysctl dfh.mode2775=2 dfh.persist2775=1 dfh.secs2775=85 dfh.run2775=1
Expected
- Stock: rig prints
.../dfh7/DF2775-SECRET-LEAK-TOKEand.../dfh7/DragonFly 6.5-DEVELOPME(freed+reclaimed rec->name read); organic runs panic instrlenon the reader's cpu (4/4 runs). - Fixed (fix.diff applied): same organic PoC -> 0 anomalies, no panic, identical sysctl output (1536 entries, correct slot contents).
DF-2775 VERDICT β reproduced (leak primitive + panic; fix validated)
Bottom line
The unsynchronized sysctl_intrnames walk is a real use-after-free read
reachable by an unprivileged reader of hw.intrnames whenever any
kernel-side register/unregister activity runs concurrently on another CPU.
On the stock INVARIANTS guest this reproduced two ways:
-
Content-controlled arbitrary-address string read (deterministic). A harness copy of the exact stock walk (kern_intr.c:1106-1110), stalled between loading a rec pointer and dereferencing
rec->name(a stand-in for any preemption/interrupt the real reader suffers there), resumed after the rec was unlinked and freed and immediately reclaimed by a same-sizeM_DEVBUFkmalloc filled with a chosenrec->name: - pointingnameat the harness token printeddfh0/.../dfh7/DF2775-SECRET-LEAK-TOKEinto the reader's entry; - pointingnameat the kernelversionstring (0xffffffff811473a0, no KASLR on this guest) printeddfh0/.../dfh7/DragonFly 6.5-DEVELOPME. Reclaim rate: 100% (kfree -> kmalloc LIFO on the same cpu, 3/3 rig runs, reclaimed=1/2/3, miss=0). -
Organic kernel panics from the unprivileged reader (4/4 runs). With the real
sysctl hw.intrnameshammered by an unprivileged reader (uid 1001, pinned cpu0 via a root launcher because USCHED_SET_CPU needs privilege) and the churn thread on cpu1 performing the interposed unregister (~200k unlinks/s), the kernel panicked every time within seconds with the identical signatureFatal trap 9 ... Stopped at strlen: cmpb $0,(%rdi)on cpuid 0 (the reader's cpu) β strlen is called fromksnprintf("%s", rec->name)(kern_intr.c:1107-1108) on a rec freed by the concurrent unregister. See panic.txt, logs/boot.after4thpanic.log, logs/hits.log.
Honest quantification
- Deterministic rig: 3/3 successful token/version captures.
- Organic panic: 4/4 runs (each within ~1-60 s at ~200k deeds/s and ~1000-1900 reads/s).
- Organic token-in-reader-output: 0 observations. Mechanism: in the
stock loop the rec-pointer load (
rec = rec->next) and therec->namedereference (argument evaluation of ksnprintf) are a few instructions apart (~5 ns). A content-controlled read needs the reader to be preempted exactly inside that window; without preemption the organic outcomes are benign (rec still valid) or crash (rec freed and its memory already sprayed/slab-flushed), which is exactly what the four panics show. The rig widens that window to prove the primitive. - Baseline with no churn: 0 anomalies across all reader runs
(logs/hits.log
t=1s reads=1927 hits=0line, pre-churn).
Privilege assessment (team directive)
- Reader side: genuinely unprivileged (hw.intrnames is 0444; systat/vmstat use it).
- Churn side: register_int/unregister_int are kernel-internal APIs; an unprivileged user cannot drive them directly. Real-world churn comes from root-driven driver activity (interface create/destroy, USB hotplug, kldload/kldunload, VPN/pps teardown).
- Ceiling: kernel heap disclosure (bounded by NUL termination of whatever the reclaimed rec->name points at) plus kernel panic (local DoS). No write primitive, no unpriv-alone trigger -> not a privesc. Severity Medium as filed is right. On this KASLR-less guest any disclosed kernel pointer would also defeat symbol randomization, but that does not change the privilege boundary.
Fix validation (mandatory: memory-safety finding reproduced)
fix.diff (pack root; identical copy in DF-2776/DF-2777 packs) rewrites
sysctl_intrnames to snapshot the rendered names per-CPU under the same
migrate+crit protocol unregister_int uses for the unlink, and performs
SYSCTL_OUT only after the snapshot. Applied to a pristine tree, kernel
rebuilt and booted (build.fixed.log.gz in DF-2777 pack):
- identical organic PoC (reader 90 s cpu0-pinned uid 1001; churn 85 s;
17,408,000 deeds at the same ~205k/s rate, 100% reclaim):
SUMMARY reads=113091 hits=0, no panic (stock: panic in seconds). - fully-real mode-0 churn (real register_int/unregister_int storm,
9.2M iterations) + reader 45 s:
reads=5829 hits=0, no panic. - output correctness preserved (src/chk.c):
rc=0 bytes=10218 entries=1536 e508=[dfp0/dfp1/dfp2/dfp3]. - the fixed protocol also defeats the deterministic stall interposition
(
DF2775-RIG-FIXEDWALK ... token-in-output=0).
Kernel references
- sys/kern/kern_intr.c:1100-1116 (unsynchronized walk)
- sys/kern/kern_intr.c:1106-1110 (rec load -> rec->name use)
- sys/kern/kern_intr.c:438-448 (unlink under crit on owning cpu only)
- sys/kern/kern_intr.c:485-487 (kfree of rec->name/rec after migrate-back)
Rig honesty notes
- The harness kernel adds
kern/df_harness.cplus (for DF-2777) two dormant hook calls in kern_intr.c; the DF-2775/DF-2776 rigs on kernel K1 used a 100% stock kern_intr.c (verified by diff before build). - The interposed unregister performs only unlink+kfree and writes the groom exclusively into memory it owns (the successful LIFO reclaim); no wild post-free stores.
- The organic panics on stock occurred with the fully-real register_int/unregister_int pair (first panic run) and with the interposed single-target deed (persist mode); all four signatures identical.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['run.log section 1-2: DF2775-RIG walker entry dfh0/..../dfh7/DF2775-SECRET-LEAK-TOKE and dfh7/DragonFly 6.5-DEVELOPME (chosen-address reads via freed rec->name)', 'panic.txt and logs/boot.after4thpanic.log: Fatal trap 9 stopped at strlen cmpb 0,(rdi) on cpuid 0 from the unprivileged reader, 4 of 4 organic runs', 'logs/hits.log: t=1s reads=1927 hits=0 pre-churn baseline; reader rate evidence', 'run.log section 7-9: fixed kernel A/B SUMMARY reads=113091 hits=0 secs=90 at 17,408,000 deeds (100 percent reclaim), mode-0 run reads=5829 hits=0, and chk.c output rc=0 bytes=10218 entries=1536 e508=[dfp0/dfp1/dfp2/dfp3]', 'fix.diff: sysctl_intrnames migrate+crit snapshot rewrite']
PoC changes
Replaced the stale seeded pack (it contained an unrelated refuted kern_time.c nanosleep candidate) with a purpose-built in-kernel harness: df_harness.c (exact stock-walk copy with a load-to-use stall point, cpu1 churn thread with interposed unregister doing kfree plus same-cpu LIFO reclaim groom, fixed-walk snapshot variant), an unprivileged O_SYNC-logging reader (df2775_reader.c) and a pin-then-setuid launcher (pin_drop.c) because USCHED_SET_CPU needs privilege. Fixed include paths (machine/atomic.h), volatile sysctl node casts, buffer bounds in the walk copy, a persist-mode chain-growth bug, and switched reader logging to O_SYNC so hits survive panic reboots.
Verified recommended fix
Snapshot hw.intrnames per CPU under int_moveto_destcpu plus crit_enter (the protocol unregister_int uses for the unlink), copy the rendered strings into a kmalloc buffer, and SYSCTL_OUT once afterwards (see fix.diff hunk sysctl_intrnames).
Verdict
Reproduced on the stock INVARIANTS guest two ways. (1) Deterministic primitive: an exact copy of the sysctl_intrnames walk (kern_intr.c:1106-1110) stalled between loading a rec pointer and using rec->name, then resumed after the rec was unlinked, freed and immediately reclaimed (kfree->kmalloc LIFO, 100 percent over 3 runs) with a chosen rec->name, printed the chosen string into the walk output: harness token DF2775-SECRET-LEAK-TOKE and the kernel version string from 0xffffffff811473a0 (arbitrary-address NUL-terminated kernel read, no KASLR). (2) Organic: an unprivileged uid 1001 reader hammering the real sysctl hw.intrnames (pinned cpu0) while kernel-side churn on cpu1 performed the interposed unregister at ~200k unlinks/s panicked the kernel in 4 of 4 runs within seconds with the identical signature Fatal trap 9 / trap 12 stopped at strlen cmpb 0,(rdi) on the reader cpu, i.e. ksnprintf percent s of rec->name (kern_intr.c:1107) on a rec freed by the concurrent unregister (kern_intr.c:486-487). Organic token capture in reader output: 0 hits, mechanically explained: the stock loop separates rec load from rec->name dereference by ~5ns so a content-controlled read needs the reader preempted exactly inside that window (the rig proves that case); organic outcomes are benign or crash, matching the 4 panics. Privilege ceiling: unpriv-readable heap disclosure plus local DoS panic; the churn side needs kernel-context (root-driven driver) activity, an unprivileged user cannot trigger alone, and there is no write primitive, so this is an info leak not a privesc. fix.diff (per-cpu migrate+crit snapshot, then a single SYSCTL_OUT) validated on a rebuilt kernel: same organic PoC gave reads=113091 hits=0 with 17.4M deeds and no panic (plus a fully-real mode-0 run reads=5829 hits=0 over 9.2M register/unregister iterations) and byte-identical sysctl output (1536 entries, correct slot contents).
No comments yet.