dm_target_uninit() tears down the target registry unconditionally: frees still-registered/still-referenced dm_target_t objects and lockuninit's the global lock β deterministic freed-module-text execution (Fatal trap 12), plus heap UAF of busied targets in the DF-4751 race window
Summary
No ref_cnt/registry validation in uninit (the EBUSY machinery exists in remove but is absent here) and dmdestroy ignores the return - compounded by kern_linker's veto-path refs-- underflow, kldunload dm fully unloads with dm_target_linear still registered. REPRODUCED on guest: seven sequential commands, zero timing, Fatal trap 12 executing freed dm.ko text. Registry-empty-veto fix validated (patched dm.ko returns EBUSY, linear unloads cleanly, guest survives).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5065 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dmt_registry_uaf.c | β | 3.7 KB | view raw | |
| run.sh | β | 147 B | view raw | |
| build.sh | β | 183 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| run.log | β | 1.3 KB | view raw | |
| panic.txt | β | 2.1 KB | view raw | |
| run_patched.log | β | 3.2 KB | view raw | |
| env.txt | β | 636 B | view raw | |
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-5065 β dm_target_uninit() tears the dm target registry down unconditionally: frees still-registered / still-referenced dm_target_t objects and destroys the registry lock
File: sys/dev/disk/dm/dm_target.c:277-295 (with the entry path in
sys/dev/disk/dm/device-mapper.c:187-197)
Severity: Medium Β· Confidence: certain Β· Reproduced: yes β
deterministic Fatal trap 12, no race, no timing (1/1 attempts).
Build (guest, as root)
# host: ship sources + prop headers, then in guest:
cc -O2 -Wall -I/tmp/propinc -o /tmp/dmt_registry_uaf dmt_registry_uaf.c -lprop
(build.sh automates both sides β the guest has libprop.so but no headers.)
Run (as root; /dev/mapper/control is 0640 root:operator)
sh /tmp/run.sh
Expected (baseline)
Seven sequential steps β no race, no timing:
| # | command | effect |
|---|---|---|
| 0 | kldload dm |
dm.ko linker refs=1 (base), userrefs=1; registry holds builtin error/zero |
| 1 | dmt_registry_uaf create df5065dev |
dm_dev_counter 0β1 (dm_dev.c:193) |
| 2 | kldunload dm |
dm_modcmd EBUSY (device-mapper.c:161) β MOD_UNLOAD veto β linker_file_unload veto path executes file->refs-- with refs==1 (kern_linker.c:514-519) β dm refs 1β0: a failed unload destroyed the base reference |
| 3 | dmt_registry_uaf remove df5065dev |
counter 1β0 (dm_dev.c:71) |
| 4 | kldload dm_target_linear |
MDT_DEPEND does dm refs++ β 0β1; refs > 1 deferral (kern_linker.c:494) can never trigger again β MODULE_DEPEND pin defeated; linear registered in dm's registry |
| 5 | kldunload dm |
refs==1 β full unload attempt β counter==0 β dmdestroy() β dm_target_uninit() (dm_target.c:284-292) frees linear's registered dm_target_t and lockuninits dm_target_mutex β then dm.ko text/data are freed |
| 6 | kldunload dm_target_linear |
dmtl_mod_handler(MOD_UNLOAD) β dm_target_remove("linear") β call into freed dm.ko text |
Observed (panic.txt): console prints Device Mapper unloaded with linear
still loaded, then
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xffffffff82604950 fault code = supervisor read instruction, page not present instruction pointer = 0x8:0xffffffff82604950 <- freed dm.ko text region current process = 870 <- the kldunload syscall
1/1 deterministic; guest down at db>.
Fix validation (same guest, vm.sh reset with-src, fix.diff applied to
/usr/src, dm.ko rebuilt -Werror-clean, identical PoC)
- step 5
kldunload dmβ "can't unload file: Device busy" β the patcheddm_target_uninitreturns EBUSY (registry non-empty),dmdestroypropagates it, dm.ko stays loaded (kldstat: Refs=0 but present). - step 6
kldunload dm_target_linearβ rc=0, clean unload through live dm text; the dependency cascade then unloads dm.ko correctly;STILL_UP.
fix_status: fixed (baseline panics, patched does not).
Relationship to known findings (not dupes)
- DF-4751 (device-mapper.c, Low, reproduced): the unlocked counter
check + unpinned dev dispatch race. This finding is the second,
independent defect at a different site:
dm_target_uninitperforms no ref_cnt/registry validation at all (contrastdm_target_remove, dm_target.c:181). It (a) survives a DF-4751 fix β an ioctl holding a looked-up target ref (dm_ioctl.c:737) is left with a freed heap object β and (b) yields a deterministic, race-free chain via the kern_linker.c veto-path refs underflow. The registry check that this file is missing is precisely the second veto that would have stopped both. - DF-2437 (dm_target_delay.c): delay's handler ignoring
dm_target_removeEBUSY. Here the EBUSY machinery itself is missing. - Cross-file note for the orchestrator:
sys/kern/kern_linker.c:518(file->refs--on a MOD_UNLOAD veto, executed even at refs==1) is the enabler of the deterministic variant and is itself an accounting bug worth a pass-2 audit of kern_linker.c.
DF-5065 VERDICT β reproduced (deterministic panic), fix validated
What was claimed
dm_target_uninit() (sys/dev/disk/dm/dm_target.c:277-295) tears down the
global target registry with zero validation: it frees every registered
dm_target_t regardless of ref_cnt (contrast dm_target_remove, which
carefully returns EBUSY at dm_target.c:181-184) and lockuninits
dm_target_mutex while registered/referenced targets exist. Its only caller
is dmdestroy() (device-mapper.c:187-197), which ignores its return value
and always returns 0 to MOD_UNLOAD.
Why it is exploitable β two reachable variants
Variant 1 β deterministic, race-free (the one reproduced here).
linker_file_unload()'s veto path does file->refs-- even when refs==1
(sys/kern/kern_linker.c:514-519) β i.e. a failed kldunload dm (EBUSY
because a dm device exists, device-mapper.c:161) destroys dm.ko's base
reference. Trace, all verified line-by-line in-tree:
kldload dmβ dm.ko refs=1 (kern_linker.c:456), userrefs=1 (:1353); builtin error/zero registered via SYSINIT (dm_target_error.c / zero.c,DM_TARGET_BUILTIN).- create a tableless device β
dm_dev_counter0β1 (dm_dev.c:193). kldunload dmβ refs==1 β not deferred (:494) βmodule_unloadβdm_modcmdEBUSY β veto β refs-- 1β0 (:518); userrefs restored (:856).- remove the device β counter 1β0 (dm_dev.c:71).
kldload dm_target_linearβ MDT_DEPEND refs++ β dm refs 0β1 (kern_linker.c:1367); therefs > 1deferral can never fire again.kldunload dmβ refs==1 β full unload attempt β counter==0 βdmdestroy()βdm_target_uninit()frees linear's registered entry (heap) and lockuninit's the registry lock β dm.ko unloaded, text freed.kldunload dm_target_linearβdm_target_remove("linear")executes freed dm.ko text β Fatal trap 12.
The SYSUNINIT builtins (dm_builtin_uninit, device-mapper.c:617-624) also
call dm_target_remove after the lock was destroyed β they survive only
because lockuninit (kern_lock.c:1325-1333) doesn't poison the still-mapped
lock and the list walks empty β benign by luck, same root cause.
Variant 2 β the DF-4751 race. Any ioctl thread holding a busied target
(dm_ioctl.c:737 lookup, ref transferred to table_en->target at :759) when
the counter check is passed leaves live tables with dangling dm_target_t
pointers β dm_table_destroy's table_en->target->destroy(...)
(dm_table.c:150-157) is a heap UAF. This variant is not separately
demonstrable on the guest (it requires the same interleaving as DF-4751 and
crashes in whichever site faults first), but the code path is unambiguous:
nothing in dm_target_uninit distinguishes ref_cnt>0 entries.
Reproduction
- Baseline:
vm.sh reset with-srcβ build (BUILD_OK) βsh /tmp/run.shβ Fatal trap 12 on the first and only attempt (no threads, no retry loops, no timing). panic.txt: IP=0xffffffff82604950, "supervisor read instruction, page not present" β the freed dm.ko text region (dm.ko was mapped at 0xffffffff82600000, size 0x6000, per kldstat in the patched run; DF-4751's freed-text signatures were in the same region). Current process = thekldunload dm_target_linearsyscall. Guest down atdb>. - The console printed
Device Mapper unloadedwhile dm_target_linear was loaded and registered β the smoking gun that uninit tore the registry down with a foreign module's entry in it.
Fix validation (fix.diff β dm_target.c + device-mapper.c, both in dm.ko)
- Applied to the guest's /usr/src,
make obj && makein sys/dev/disk/dm (-Werror clean), dm.ko installed, guest fresh otherwise. - Identical PoC (
sh /tmp/run.sh): step 5 now printskldunload: can't unload file: Device busy(dm_target_uninit β EBUSY β dmdestroy propagates β dm_modcmd vetoes); kldstat shows dm.ko Refs=0 but loaded; step 6kldunload dm_target_linearreturns 0 cleanly through live text; the dependency cascade then unloads dm.ko itself (correct order);STILL_UP. - Design note: the fix also makes
dmdestroyunregister the builtin error/zero targets explicitly before the registry check (their SYSUNINITs run after the MOD_UNLOAD handler, so without this the "registry must be empty" rule would make dm permanently unloadable). Normal unload keeps working β demonstrated by the clean cascade unload at the end of the patched run. Known cosmetic degradation: if the veto fires, error/zero stay unregistered while dm remains loaded (tables using them would then fail to load); strictly safer than the UAF, and only reachable in the corrupted-refs state. - fix_status: fixed β baseline panics, patched does not, exact same PoC.
Exploit chain ceiling
Trigger side needs root (kldload/kldunload; kern.securelevel must be
β€0 β it is -1 on the guest and by default). The ioctl side alone is
operator-group reachable (0640 root:operator, device-mapper.c:181), but the
deterministic chain needs the privileged unload. Impact: freed-kernel-text
execution (deterministic) + freed-heap dm_target_t use (race variant) β
a code-execution-class primitive, but groomed by root against root β
Medium (deterministic kernel panic at minimum; the memcorrupt-class
primitive is real and was demonstrated as a trap-12).
Honesty notes
attempts= 1 baseline + 1 patched; deterministic chain hit on the first try both times.- The kern_linker.c:518 refs-- enabler is out of this finding's file; the finding is the missing in-file validation (the second veto), and the fix stands alone: with it, the chain dies even with the linker bug present (proven on the guest). The linker bug should still get its own finding β it affects every module that vetoes unloads.
- Guest reset
with-srcperformed after the runs; nosys/file in the audit tree was modified (fix.diff authored against the read-only tree, applied only inside the disposable guest).
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 (dm_target.c non-empty-registry EBUSY + device-mapper.c explicit builtin unregistration and error propagation; both files ship in dm.ko) applied to a fresh with-src guest, module rebuilt -Werror-clean, installed; the IDENTICAL PoC no longer panics: the fatal kldunload dm is vetoed with 'Device busy' while dm_target_linear is registered, linear then unloads cleanly through live dm text, and the dependency cascade unloads dm.ko correctly afterwards (STILL_UP). Normal dm unload still works (demonstrated by the clean cascade at end of patched run).
run_patched.log (full trace: veto at step 5, clean unload at step 6, STILL_UP); fix.diff; VERDICT.md fix-validation section
Confirmed kernel references
- sys/dev/disk/dm/dm_target.c:277
- sys/dev/disk/dm/dm_target.c:284
- sys/dev/disk/dm/dm_target.c:287
- sys/dev/disk/dm/dm_target.c:292
- sys/dev/disk/dm/device-mapper.c:161
- sys/dev/disk/dm/device-mapper.c:187
- sys/dev/disk/dm/device-mapper.c:194
- sys/dev/disk/dm/dm_target_error.c:73
- sys/kern/kern_linker.c:494
- sys/kern/kern_linker.c:514
- sys/kern/kern_linker.c:518
Detail
Exploit chain
kldload dm -> create device (counter=1) -> kldunload dm (EBUSY veto; linker veto-path refs-- destroys dm.ko's base ref) -> remove device (counter=0) -> kldload dm_target_linear (dep ref on underflowed dm) -> kldunload dm (full unload: registry freed with linear's entry in it, dm.ko freed) -> kldunload dm_target_linear -> call into freed dm.ko text -> Fatal trap 12. Escalation ceiling: freed-kernel-text execution primitive groomed by root against root; treated as deterministic-panic + memcorrupt-class correctness bug (Medium).
Evidence (decisive lines)
panic.txt (Fatal trap 12, IP in freed dm.ko text, 'Device Mapper unloaded' printed with linear loaded); run.log (chain narration); run_patched.log (identical PoC on fix.diff: kldunload dm vetoed 'Device busy', linear unloads cleanly, guest STILL_UP); fix.diff (dm_target.c + device-mapper.c, -Werror clean module build); VERDICT.md (full line-by-line trace + fix-validation narrative)
PoC changes
Harness is a de-threaded adaptation of the DF-4751 proplib client (create/remove only); the deterministic chain replaced DF-4751's churn/race with seven sequential shell steps; fix.diff regenerated mechanically after hand-written hunk counts failed patch --dry-run.
Verified recommended fix
dm_target_uninit: return EBUSY when the registry is non-empty (any registered or referenced target) instead of freeing everything + lockuninit; dmdestroy: unregister builtin error/zero targets explicitly before the check and propagate uninit's error so MOD_UNLOAD vetoes.
Verdict
Reproduced 1/1 deterministic (no race, no timing): on a fresh with-src guest, seven sequential steps -- kldload dm; create tableless device; kldunload dm (EBUSY veto, kern_linker.c:518 refs-- 1->0); remove device; kldload dm_target_linear (dep ref 0->1, MODULE_DEPEND pin permanently defeated); kldunload dm (SUCCEEDS: dm_target_uninit at dm_target.c:284-287 frees linear's registered dm_target_t and lockuninit's the registry, dm.ko text freed); kldunload dm_target_linear -- executes dm_target_remove() through freed dm.ko text: Fatal trap 12, IP=0xffffffff82604950 (supervisor read instruction, page not present; dm.ko was mapped at 0xffffffff82600000), guest down at db>. Root cause is in-file: dm_target_uninit performs no ref_cnt/registry validation at all (contrast dm_target_remove's EBUSY at dm_target.c:181) and its caller dmdestroy() ignores its return. Race variant: an ioctl holding a looked-up target ref (dm_ioctl.c:737) when MOD_UNLOAD slips past the counter check gets a freed heap dm_target_t used by dm_table_destroy (dm_table.c:150-157) -- same missing-validation defect, not separately demonstrable from DF-4751's crash sites. Trigger needs root (kldload/kldunload, securelevel<=0); ioctl side is operator-group reachable.
No comments yet.