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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2776 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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βintrread from freed memory selected the walk; the%sread disclosed the string at the chosen kernel address to the console/dmesg. - Wild variant (
dfh.wild2776=1,name=0x5151515151515150): panicStopped 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).
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:
- reads
((intrec_t)id)->intrfrom freed heap at kern_intr.c:425 β the value that selects whichintr_block->ary[cpuid][intr]list to walk; - on the not-found path prints
((intrec_t)id)->namewith%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
- sys/kern/kern_intr.c:423-428 (cookie deref before validation)
- sys/kern/kern_intr.c:440-444 (walk selected by freed-sourced intr)
- sys/kern/kern_intr.c:485-491 (not-found path printing through the cookie)
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_testableConfirmed 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.
No comments yet.