dimm_mod_unload() drains dimm_softc references owned by still-attached drivers, kfree()ing live objects β victim use-after-free into freed heap/unmapped module text (proven kernel panic)
Summary
The MOD_UNLOAD loop treats 'entry still present' as a bug to drain instead of a busy veto - kldunload dimm silently destroys objects ecc_e3/memtemp_* still hold; the KASSERT is compiled out on production kernels and kern_linker only protects MODULE_DEPEND users (ecc_e3.c:156 omits it). REPRODUCED on guest: kldunload dimm returns 0 with a live creator holding the only ref, then the victim destroy faults on unmapped dimm.ko text (Fatal trap 12) - control with MODULE_DEPEND fully protected; EBUSY-veto fix validated. Root-gated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5011 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.5 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| drainpoc.c | β | 2.1 KB | view raw | |
| build.sh | β | 141 B | view raw | |
| run.sh | β | 90 B | view raw | |
| run-control.sh | β | 740 B | view raw | |
| run-fix.sh | β | 778 B | view raw | |
| build.log | β | 2.8 KB | view raw | |
| run.log | β | 3.2 KB | view raw | |
| run.2.log | β | 1.5 KB | view raw | |
| run.3.log | β | 1.6 KB | view raw | |
| panic.txt | β | 2.1 KB | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 5.7 KB | view raw |
DF-5011 β dimm(4) dimm_mod_unload() drains references it does not own β UAF/panic
Kernel module API defect in sys/dev/misc/dimm/dimm.c, demonstrated end-to-end on
the QEMU guest: kldunload dimm while any KPI user still holds a dimm_softc
reference silently drains the user's reference count and kfrees the live
object; the user's next call into the dimm KPI (destroy, or an ECC/thermal
sensor update) is a use-after-free into freed heap / unmapped module text β
proven kernel panic (Fatal trap 12).
Files
drainpoc.cβ PoC kernel module source (mirrorsecc_e3.c: uses the dimm KPI withoutMODULE_DEPEND).build.shβ exact module build recipe (3 variants + fixed dimm.ko).run.shβ baseline reproduction (bug β drain β panic).run-control.shβ control: same module WITHMODULE_DEPEND(protected).run-fix.shβ fix validation withfix.diff-patcheddimm.ko.build.logβ compiler output (final successful builds).run.logβ baseline transcript (drain + panic, full serial console).run.2.logβ control transcript.run.3.logβ fix-validation transcript.panic.txtβ kernel panic from the guest serial console.env.txtβ guest environment.fix.diffβ git-apply-able fix againstsys/dev/misc/dimm/dimm.c.
Precondition (important)
dimm_mod_unload() only runs when dimm is a loadable module. The stock
X86_64_GENERIC kernel compiles dimm in (device dimm, sys/config/X86_64_GENERIC:189),
where MOD_UNLOAD never runs. The bug is reachable on kernels built without
device dimm where dimm.ko is pulled in by memtemp_e5/ecc_e5/ecc_e3
modules (this is exactly what /boot/kernel/dimm.ko being shipped is for).
For the PoC the guest kernel was rebuilt without device dimm:
# in guest (DragonFly 6.5-DEVELOPMENT, /usr/src present):
sed -i '' -e '189s/^/#/' /usr/src/sys/config/X86_64_GENERIC # device dimm
sed -i '' -e '191s/^/#/' /usr/src/sys/config/X86_64_GENERIC # device ecc
sed -i '' -e '193s/^/#/' /usr/src/sys/config/X86_64_GENERIC # device memtemp
cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC && \
make installkernel KERNCONF=X86_64_GENERIC && shutdown -r now
(ecc/memtemp must go too β they reference dimm_* symbols at link time.)
Build
See build.sh. Three PoC module variants are produced from drainpoc.c by
sed (the module name must be baked in; DECLARE_MODULE's internal ##
pasting defeats macro-renaming, and CFLAGS+= in a kmod Makefile does not
reach the compiler):
drainpoc.koβ moduledrainpoc, NOMODULE_DEPEND(dimm)(bug path)deppoc.koβ moduledeppoc, WITHMODULE_DEPEND(dimm,1,1,1)(control)pocfix.koβ modulepocfix, NO dep (used against the fixed dimm.ko)
Plus dimm-fixed.ko = stock dimm.c + fix.diff.
Run (baseline β expect panic)
# guest, root:
kldload dimm # module-mode dimm (shipped dimm.ko)
kldload /root/df5011/drainpoc.ko # creates dimm_softc, holds ref=1
sysctl hw.dimminfo # hw.dimminfo.dimm0.* present
kldunload dimm # BUG: rc=0 β drains+free live softc
sysctl hw.dimminfo # "unknown oid" (object destroyed)
kldunload drainpoc # dimm_destroy on freed object +
# call into unmapped dimm.ko text
# -> Fatal trap 12, system down
Expected result (all observed, see run.log / panic.txt)
kldunload dimmreturns 0 although a live creator holds the only reference (should be refused β the module owns no references of its own).hw.dimminfo.dimm0.*disappears: the drain loop randimm_destroy()on the user's softc (kfree at dimm.c:195, sysctl teardown at :193-194).- The victim's next KPI call faults:
Fatal trap 12: page fault while in kernel mode,fault virtual address = 0xffffffff826004a0(= dimm.ko text base0xffffffff82600000+ 0x4a0),supervisor read instruction, page not presentβ DDB, system dead.
Fix validation (run.3.log)
With fix.diff applied to dimm.ko:
* kldunload dimm-fixed with pocfix holding a live ref β "Device busy"
(rc=1), console: dimm0 is still referenced (ref 1); cannot unload.
* hw.dimminfo.dimm0.* still present; victim can destroy its ref properly.
* After the victim's clean destroy, kldunload dimm-fixed β rc=0.
* No panic, no drain.
VERDICT β DF-5011: dimm_mod_unload() drains foreign dimm_softc references (UAF β panic)
- file : sys/dev/misc/dimm/dimm.c
- root cause : dimm.c:305-321 (
dimm_mod_unload) β the MOD_UNLOAD loop callsdimm_destroy(TAILQ_FIRST(...))until the list is empty; every call decrements a reference OWNED BY A KPI USER (dimm_destroy: dimm.c:183-188) and frees the softc at dimm.c:190-195 when it reaches zero. The framework itself holds no references, so "list non-empty at unload" ALWAYS means a driver (ecc_e3/ecc_e5/memtemp_core/memtemp_e5 or any out-of-tree user) still holds livestruct dimm_softc *pointers. - reproduced : YES β baseline panic on the QEMU guest (run.log, panic.txt)
- impact : kernel panic (root-triggered; module-unload path)
- fix validated : YES β fix.diff (EBUSY veto), behavior contrast in run.3.log
Attack surface / threat model
dimm.c has zero unprivileged entry points (no /dev node, no ioctls, no packet
path; sysctl writes are gated by SYSCAP_NOSYSCTL_WR β kern_sysctl.c:1446).
The defect is on the module-administration path: it takes an privileged user
(root) to run kldunload dimm. The reason it is still a real kernel bug rather
than "root did it to itself": the dimm KPI contract is dimm_create() returns a
refcounted handle, dimm_destroy() releases ONE reference, and the owner
expects the object to live until its OWN destroy. dimm.c unilaterally violates
that contract from a third party's kldunload. In-tree victim: ecc_e3.c
(sys/dev/misc/ecc/ecc_e3.c:387 uses dimm_create; :156-157 declares
MODULE_DEPEND only on pci and coremctl β NOT on dimm), so on E3 hardware with
module-mode dimm, kldunload dimm while ecc_e3 is attached+live frees ecc_e3's
softcs; the next ECC interrupt calls dimm_sensor_ecc_add() on freed memory /
unmapped module text β panic or silent heap corruption. (ecc_e5/memtemp_* do
declare MODULE_DEPEND and are protected by the linker's refs β verified live in
run.2.log: "Device busy".)
Why the linker does not save dep-less users
kern_linker.c:477-518 linker_file_unload(): with no MODULE_DEPEND there is no
reference on dimm.ko, so kldunload dimm runs module_unload β
dimm_mod_event(MOD_UNLOAD) β the drain. DragonFly's kldunload has no -f
flag; the missing dependency is the enabler.
What was run (all as root on the guest)
Kernel: custom X86_64_GENERIC without device dimm/device ecc/device
memtemp (nativekernel build; necessary because the stock generic kernel
compiles dimm in, where MOD_UNLOAD never runs). Stock shipped /boot/kernel/dimm.ko
was the module under test (rebuilt by installkernel from unmodified source).
BASELINE (run.log):
kldload dimm β ok
kldload drainpoc.ko (no MODULE_DEPEND) β "sc=0xfffff800899b0800 ref=1"
sysctl hw.dimminfo β dimm0 present
kldunload dimm β rc=0 <-- CONTRACT VIOLATION
sysctl hw.dimminfo β "unknown oid" (softc kfreed)
kldunload drainpoc β MOD_UNLOAD calls dimm_destroy:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffffffff826004a0 (dimm.ko text + 0x4a0)
instruction pointer = 0x8:0xffffffff826004a0
fault code = supervisor read instruction, page not present
β all CPUs stopped, DDB prompt, system down.
(Serial console proves ordering: the drain+free happened silently during
kldunload dimm; the fault is the victim's call, exactly what an ecc_e3
ECC interrupt would do.)
CONTROL (run.2.log) β same PoC WITH MODULE_DEPEND(dimm,1,1,1): kldload deppoc.ko β dimm.ko AUTO-loaded as dependency kldunload dimm (deppoc live) β "can't unload file: Device busy" kldunload deppoc β rc=0, clean destroy, dimm released β isolates the defect to dimm.c's own unload path + dep-less users.
FIX VALIDATION (run.3.log) β fix.diff applied to dimm.ko, dep-less pocfix: kldunload dimm-fixed (pocfix live) β "Device busy", console: "dimm0 is still referenced (ref 1); cannot unload" hw.dimminfo.dimm0.node still readable; pocfix's own destroy works (dimm still mapped); final kldunload dimm-fixed β rc=0 with empty list. No drain, no free, no panic.
Exploitability ceiling
The primitive is a kernel use-after-free/write (refcount dec + TAILQ_REMOVE +
kfree of an object still in use), but the trigger is privileged
(kldunload). No unprivileged path exists. Realistic worst case: root-caused
kernel panic or heap corruption on ECC-capable servers during module
administration. Severity Low (per rubric: "requires already-privileged user").
Classification
status : reproduced reproduced : 1 impact : panic confidence : certain fix_status : fixed (baseline drain/panic gone with patch; clean lifecycle kept)
Kernel references
sys/dev/misc/dimm/dimm.c:305-321 buggy unload loop (drain) sys/dev/misc/dimm/dimm.c:178-199 dimm_destroy decrements user's ref, frees at :195 sys/dev/misc/ecc/ecc_e3.c:156-157 missing MODULE_DEPEND(ecc_e3, dimm) (enabler) sys/kern/kern_linker.c:477-518 unload refs come only from MODULE_DEPEND sys/config/X86_64_GENERIC:189 device dimm (why stock guest is inert here)
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
fixedfix.diff (dimm_mod_unload -> int, EBUSY veto while list non-empty, error propagated from MOD_UNLOAD; forward prototype at dimm.c:75 updated too) built as dimm-fixed.ko in-guest and validated: with a dep-less user holding ref=1, kldunload dimm-fixed fails with 'Device busy' and logs 'dimm0 is still referenced (ref 1); cannot unload'; the softc and its sysctl tree survive; the user then destroys its own reference cleanly and the module unloads rc=0 afterwards. Baseline behavior (silent drain + kfree + victim panic) is gone. fix_patched_reproduced=false means the BUG does NOT reproduce on the patched module (the fix works), per the schema semantics.
run.3.log (full fixed-module transcript), build.log (dimm-fixed.ko build), fix.diff (git apply --check passes against sys/)
Confirmed kernel references
Detail
Exploit chain
root loads module-mode dimm.ko (kernel without device dimm; stock /boot/kernel/dimm.ko) -> dep-less KPI user (ecc_e3 pattern) calls dimm_create() and holds ref=1 -> root kldunload dimm -> linker has no dep ref -> MOD_UNLOAD -> dimm_mod_unload() drains the user's ref (1->0), frees sysctl ctx, kfrees softc, unmaps dimm.ko -> victim's next KPI call (dimm_destroy at detach, dimm_sensor_ecc_add on ECC interrupt) -> instruction fetch from unmapped module text / write to freed heap -> Fatal trap 12 -> panic, system down. Unprivileged trigger: none (no unprivileged entry point in dimm.c); ceiling is root-triggered panic/heap corruption.
Evidence (decisive lines)
run.log (baseline: kldunload dimm rc=0 with live ref, hw.dimminfo destroyed, then panic), panic.txt (Fatal trap 12, fault VA=0xffffffff826004a0 = dimm.ko+0x4a0, DDB), run.2.log (control with MODULE_DEPEND: Device busy, clean teardown), run.3.log (fix validated: veto + intact object + clean lifecycle), build.log, fix.diff (applies clean to sys/)
PoC changes
The orchestrator provided no seed PoC; authored from scratch. PoC module builds need: (1) module name baked in via sed (DECLARE_MODULE's internal ## pasting defeats macro renaming; CFLAGS+= in a kmod Makefile does not reach the compiler β the initial -DWANT_DEP build silently produced a dep-less control, which the first run exposed and which was rebuilt+verified via strings/MDT metadata); (2) compiling dimm.c itself as a kmod requires kernel-generated *_if.h copied from /usr/obj/usr/src/sys/X86_64_GENERIC into the kmod obj dir; (3) the bug path requires a kernel without device dimm (stock X86_64_GENERIC compiles dimm in, where MOD_UNLOAD never runs) β built in-guest with make nativekernel after commenting device dimm/ecc/memtemp.
Verified recommended fix
dimm_mod_unload(): return EBUSY when any dimm_softc still exists (veto the module unload) instead of looping dimm_destroy() until the list is empty; propagate the error from MOD_UNLOAD (see fix.diff); optionally add the missing MODULE_DEPEND(ecc_e3, dimm, 1,1,1).
Verdict
REPRODUCED (panic). sys/dev/misc/dimm/dimm.c:305-321 dimm_mod_unload() loops dimm_destroy() until the dimm_softc list is empty; since dimm_destroy() decrements a reference owned by a KPI user (dimm.c:184) and kfrees the softc at zero (dimm.c:195), module unload silently drains FOREIGN references and frees live objects. Demonstrated on the guest with a dep-less KPI user (mirroring in-tree ecc_e3.c, which omits MODULE_DEPEND on dimm): kldunload dimm returned 0, tore down hw.dimminfo and kfree()d the victim's softc; the victim's subsequent dimm_destroy() call faulted on the unmapped dimm.ko text (Fatal trap 12, fault VA = dimm.ko base 0xffffffff82600000 + 0x4a0) and the system died at the DDB prompt. A control module WITH MODULE_DEPEND(dimm,1,1,1) is fully protected (kldunload -> Device busy; auto-load of dimm.ko observed), isolating the defect to dimm.c's own unload path for dep-less users. Root-gated (kldunload) => severity Low; the same primitive is freed-object memory corruption (refcount dec + TAILQ_REMOVE + kfree of a live object) on ECC servers. fix.diff (refuse unload with EBUSY while any softc exists) validated in-guest: drain/free gone, correct lifecycle preserved.
No comments yet.