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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5011 Β· 16 files
FileTypeDescriptionSize
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 (mirrors ecc_e3.c: uses the dimm KPI without MODULE_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 WITH MODULE_DEPEND (protected).
  • run-fix.sh β€” fix validation with fix.diff-patched dimm.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 against sys/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 β€” module drainpoc, NO MODULE_DEPEND(dimm) (bug path)
  • deppoc.ko β€” module deppoc, WITH MODULE_DEPEND(dimm,1,1,1) (control)
  • pocfix.ko β€” module pocfix, 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)

  1. kldunload dimm returns 0 although a live creator holds the only reference (should be refused β€” the module owns no references of its own).
  2. hw.dimminfo.dimm0.* disappears: the drain loop ran dimm_destroy() on the user's softc (kfree at dimm.c:195, sysctl teardown at :193-194).
  3. The victim's next KPI call faults: Fatal trap 12: page fault while in kernel mode, fault virtual address = 0xffffffff826004a0 (= dimm.ko text base 0xffffffff82600000 + 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.md
↓ download raw

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 calls dimm_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 live struct 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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/)
↓ fix.diffper-fix-DF-5011

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.