β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2776

unregister_int dereferences the caller-supplied (possibly stale/freed) cookie before and after validation β†’ freed-heap read and arbitrary-pointer %s print

Field Value
ID DF-2776
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:H
CWE CWE-476 / CWE-416
File sys/kern/kern_intr.c
Lines 425, 489-490 (nexus.c:538-545 no teardown guard)
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

unregister_int() reads intr = ((intrec_t)id)->intr before any validation of id, and when the record is not found it prints ((intrec_t)id)->name as %s β€” both dereference the cookie after it may already have been freed by a previous teardown. nexus_teardown_intr passes the cookie straight through with no double-teardown guard and never clears it, so any driver calling bus_teardown_intr twice with a stale cookie (a classic driver error-path bug) makes the kernel read freed heap and print a string through whatever pointer now occupies rec->name β€” arbitrary-address string read to console/log β€” or panic.

Threat model & preconditions

Requires a buggy driver performing double teardown (root-triggered detach or shutdown error paths); the kernel turns the driver bug into an arbitrary-pointer string print instead of ignoring the stale cookie.

Treat a not-found id as a benign no-op without dereferencing it (print the pointer, not the name): see row diff.

Timeline

  • 2026-08-31 Discovered during pass-2 audit of kern_intr.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2776 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
run.log β€” 1.8 KB view raw
panic.txt β€” 466 B view raw
df2776_groomed.dmesg β€” 599 B ↓ download
df2776_fixed.dmesg β€” 267 B ↓ download
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
verdict.json β€” 3.3 KB view raw

DF-2776 evidence pack β€” unregister_int dereferences the stale caller cookie

Finding: sys/kern/kern_intr.c:425 β€” intr = ((intrec_t)id)->intr; reads through the caller-supplied cookie BEFORE any validation (the only prior check is the cpuid range assert), and kern_intr.c:489-490 prints ((intrec_t)id)->name with %s on the not-found path. If a driver calls unregister_int with a stale (already-freed) cookie β€” e.g. a double-unregister bug β€” the kernel reads intr from freed heap memory and prints a NUL-terminated string from whatever address the freed cookie's name field now holds.

Rig

Kernel harness (src/df_harness.c, sysctl dfh.run2776): register an intrec on softint 253 cpu0, call the REAL unregister_int (frees name+rec), probe kmalloc(56, M_DEVBUF) (LIFO reclaim of the freed rec β€” reuse=YES observed), fill the cookie memory as an attacker-influenced reallocation would (intr=253, name=&df_token_2776), then call the real unregister_int again with the stale cookie.

Expected / observed

  • Stock (df2776_groomed.dmesg, run.log): warning: unregister_int: int 253 handler for DF2776-STALE-COOKIE-READ-4b77d1 not found β€” intr read from freed memory selected the walk; the %s read disclosed the string at the chosen kernel address to the console/dmesg.
  • Wild variant (dfh.wild2776=1, name=0x5151515151515150): panic Stopped at strlen: cmpb $0,(%rdi) mid-print (panic.txt).
  • Fixed (df2776_fixed.dmesg): warning: unregister_int: int handler 0xfffff8004f164290 not found β€” pointer only, no cookie dereference; the wild variant returns cleanly with no panic.

Build/run: same kernel-harness procedure as DF-2775 (README there); sysctl dfh.wild2776=0 dfh.run2776=1 then sysctl dfh.wild2776=1 dfh.run2776=1. fix.diff = canonical combined fix (DF-2776 hunk: the not-found kprintf).

VERDICT.md
↓ download raw

DF-2776 VERDICT β€” reproduced (stale-cookie read + console disclosure + panic; fix validated)

Bottom line

unregister_int treats the caller-supplied cookie as live memory before and after validation. With a stale (freed) cookie the stock kernel:

  1. reads ((intrec_t)id)->intr from freed heap at kern_intr.c:425 β€” the value that selects which intr_block->ary[cpuid][intr] list to walk;
  2. on the not-found path prints ((intrec_t)id)->name with %s (kern_intr.c:489-490) β€” a NUL-terminated read from whatever address the freed cookie now holds.

Reproduced on the stock INVARIANTS guest with the in-kernel rig (dfh.run2776): register -> real unregister (free) -> LIFO reclaim of the freed intrec (kmalloc(56)=... reuse=YES, same address observed) -> fill intr=253, name=&df_token_2776 -> real unregister_int(stale):

warning: unregister_int: int 253 handler for DF2776-STALE-COOKIE-READ-4b77d1 not found

The intr value 253 came from the freed cookie (nothing else supplied it) and the printed string came from the chosen kernel address (harness .data). The wild variant (name=0x5151515151515150) panicked the kernel inside the kprintf, console showing the message truncated mid-%s:

warning: unregister_int: int 253 handler for Stopped at      strlen: cmpb    $0,(%rdi)

Threat model honesty

The trigger requires a kernel-context caller to hand unregister_int a stale cookie β€” i.e. a driver bug (double unregister, use-after-free of its own handle). An unprivileged user cannot reach this directly. The impact is (a) kernel-memory disclosure to the console/log (one NUL-terminated string per bad call, chosen-address once heap reuse is influenced) and (b) kernel panic (local DoS) when the stale name is invalid. Severity Low as filed is right.

Note: the intr load at :425 cannot be removed without an API change (the cookie is the only way to find the list); the fix removes the print-through of the stale pointer, which is the disclosure/crash vector.

Fix validation

fix.diff hunk (identical file in all three packs):

-   kprintf("warning: unregister_int: int %d handler for %s not found\n",
-       intr, ((intrec_t)id)->name);
+   kprintf("warning: unregister_int: int handler %p not found\n", id);

On the rebuilt fixed kernel both variants print the pointer only (warning: unregister_int: int handler 0xfffff8004f164290 not found), the wild variant returns without panicking, and the guest stays alive (df2776_fixed.dmesg). Stock-vs-fixed A/B is exact: same rig, same sysctl invocation.

Kernel references

Rig honesty notes

  • The reclaim probe writes the fake cookie fields only into memory it owns (the successfully reclaimed block). The stock kernel performs both dereferences itself (:425 read observed via the printed int 253, :490 read observed via the disclosed string / the panic).
  • kern_intr.c on the K1 build was byte-identical to stock (diff-verified before build); the fixed build is pristine + fix.diff (+ the dormant DF-2777 hook lines, which are no-ops unless armed).

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_testable
↓ fix.diffper-fix-DF-2776

Confirmed kernel references

Detail

Evidence (decisive lines)

['df2776_groomed.dmesg: reuse=YES probe then warning: unregister_int: int 253 handler for DF2776-STALE-COOKIE-READ-4b77d1 not found', 'panic.txt: warning: unregister_int: int 253 handler for Stopped at strlen cmpb 0,(rdi) -- kprintf died mid percent-s on the poisoned stale name pointer', 'df2776_fixed.dmesg: pointer-only message for both groomed and wild variants, guest still alive', 'run.log: full stock and fixed A/B transcript']

PoC changes

Built the rig into the in-kernel harness df_harness.c (runtime kldload GPFs on this guest); the reclaim probe writes fake cookie fields only into memory it owns (successful LIFO kmalloc reclaim, verified by printed address equality).

Verified recommended fix

Print the cookie pointer, never dereference it, on the not-found path of unregister_int (fix.diff hunk: kprintf warning: unregister_int: int handler %p not found).

Verdict

Reproduced on the stock INVARIANTS guest. The in-kernel rig (sysctl dfh.run2776) registered an intrec via the real register_int, freed it with the real unregister_int, reclaimed the freed 56-byte intrec with an immediate same-cpu kmalloc (reuse=YES, identical address printed), filled the cookie memory with intr=253 and name=&token, then called the real unregister_int with the stale cookie. The kernel read intr from the freed cookie at kern_intr.c:425 (the walk used intr 253, sourced solely from freed memory) and, on the not-found path, printed warning: unregister_int: int 253 handler for DF2776-STALE-COOKIE-READ-4b77d1 not found, disclosing the string at the chosen kernel address to the console/dmesg (kern_intr.c:489-490). The wild variant (name=0x5151515151515150) panicked inside the kprintf: the console shows the message truncated mid percent-s followed by Stopped at strlen cmpb 0,(rdi). Threat model: requires a kernel-context caller to pass a stale cookie (driver double-unregister style bug); unprivileged users cannot trigger it directly, so the ceiling is console kernel-memory disclosure plus a kernel panic (DoS) from an already-privileged code path; severity Low as filed. Fix validated on a rebuilt kernel: the not-found path prints the pointer only (warning: unregister_int: int handler 0xfffff8004f164290 not found), never dereferences the cookie, and the wild variant returns cleanly with the guest alive.