NULL-deref kernel panic when dm backing path resolves to a non-device vnode
Summary
dm_dk_lookup() opens any vnode type via vn_open() with no check result is block device then dm_pdev_insert() unconditionally feeds pdev_vnode->v_rdev to dev_dioctl(). For regular file (or any non-VBLK/VCHR) path v_rdev is NULL so dev_dioctl() dereferences NULL inside dev_needmplock() kernel panics. Rest of dm target layer already knows v_rdev can be NULL (linear:132 striped:398/435 crypt:1017 all guard with if NULL return ENXIO) but insert-time DIOCGPART call missed. Device name fully attacker-controlled flows from DM_TABLE_PARAMS via prop_dictionary_get_cstring -> strsep argv[0] -> dm_pdev_insert with no validation it names a device. Local DoS kernel panic from operator group /dev/mapper/control.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2450 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | libprop NETBSD_DM_IOCTL PoC: linear reload with regular-file backing path -> NULL v_rdev panic | 6.4 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c -lprop | 89 B | view raw |
| run.sh | run-script | kldload dm; ./poc | 236 B | view raw |
| build.log | build-log | successful build output | 86 B | view raw |
| run.log | run-log | panic run output (ssh dropped, panic signature) | 1018 B | view raw |
| panic.txt | panic-signature | Fatal trap 12, fault va=0xa8, Stopped at dev_dioctl+0xc | 1.5 KB | view raw |
| fix_run.log | fix-run-log | fixed dm.ko: reload rv=2 (ENOENT), no panic | 914 B | view raw |
| fix.diff | suggested-fix | check v_rdev != NULL before dev_dioctl in dm_pdev_insert | 640 B | view raw |
| VERDICT.md | verdict | full analysis | 2.8 KB | β raw |
| README.md | readme | build/run instructions | 1.0 KB | β raw |
| env.txt | environment | uname, cc version, devfs perms | 359 B | view raw |
DF-2450: dm_pdev NULL-deref via non-block-device backing path
Build & Run
Setup (as root)
kldload dm cc -O2 -o poc poc.c -lprop
Trigger (as root β control dev is 0640 root:operator)
./poc
Expected Output (unpatched #0 kernel)
Kernel panic:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xa8 Stopped at dev_dioctl+0xc: movq 0xa8(%rdi),%rax
The reload ioctl never returns; guest dies.
Expected Output (fixed dm.ko)
[!] reload rv=2 (No such file or directory) -- kernel survived
Clean ENOENT; guest stays up.
Mechanism
Reloading a linear table with a regular file as the backing path causes
dm_pdev_insert to vn_open the file (succeeds), then call
dev_dioctl(vnode->v_rdev=NULL, DIOCGPART) β NULL v_rdev deref.
The finding said "path does NOT resolve (e.g. /dev/nonexistent)"; that case is handled cleanly (dm_dk_lookup fails, returns NULL, linear returns ENOENT). The actual trigger is a path that resolves to a non-block-device vnode.
DF-2450 VERDICT β dm_pdev NULL-deref via non-block-device backing path
Verdict: REPRODUCED (panic) β rootβkernel DoS; fix VALIDATED
Summary
Reloading a dm linear table whose backing path resolves to a non-block-device
vnode (regular file) triggers a NULL-deref kernel panic in dm_pdev_insert
(sys/dev/disk/dm/dm_pdev.c:168).
The finding's description said "path does NOT resolve (e.g. /dev/nonexistent)".
That specific case is handled cleanly (dm_dk_lookup fails β dm_pdev_insert
returns NULL β linear target returns ENOENT). The actual trigger is a path
that does resolve via vn_open but whose vnode is not a device (v_rdev==NULL),
e.g. a regular file. The underlying bug (NULL v_rdev dereferenced in
dev_dioctl) is real and is in dm_pdev.c as cited.
Mechanism (trigger β primitive β effect)
- Attacker opens
/dev/mapper/control(0640 root:operator β root/operator only) and sendscreate+reloadioctls (libprop NETBSD_DM_IOCTL). - The
reloadspecifies target typelinearwith params<path> 0where<path>is a regular file (e.g./tmp/df2450_backing). dm_table_load_ioctl(dm_ioctl.c:783) callsdm_target_linear_init(dm_target_linear.c:56).dm_target_linear_initcallsdm_pdev_insert(argv[0])(line 67).dm_pdev_insert(dm_pdev.c:120) callsdm_dk_lookup(line 143) βvn_opensucceeds for the regular file.dm_pdev_insertthen calls line 168:c error = dev_dioctl(dmp->pdev_vnode->v_rdev, DIOCGPART, ...);For a regular-file vnode,v_rdevis NULL.dev_dioctl(NULL, ...)reads0xa8(%rdi)where%rdi==NULLβ page fault at virtual address 0xa8.
Panic signature
Fatal user address access from kernel mode from poc at ffffffff8062cd5c Fatal trap 12: page fault while in kernel mode fault virtual address = 0xa8 Stopped at dev_dioctl+0xc: movq 0xa8(%rdi),%rax
Exploit chain
N/A β valid hard blocker (root-only reachability). /dev/mapper/control
is 0640 root:operator; maxx (uid 1001) is not in operator/wheel. The bug
requires root to trigger. There is no unprivileged path. NULL-deref is not a
write primitive, so even from root there is no escalation chain. This is a
rootβkernel DoS / hardening gap.
Fix
Add a NULL check for v_rdev in dm_pdev_insert after dm_dk_lookup succeeds
(dm_pdev.c:151), rejecting non-device vnodes cleanly (returns NULL β caller
returns ENOENT). See fix.diff.
Fix validation
- Unpatched (#0 baseline): PoC panics the kernel (Fatal trap 12,
Stopped at dev_dioctl+0xc). - Patched (fixed dm.ko): Same PoC returns
reload rv=2 (ENOENT)β clean rejection, no panic. Guest stays up.
Fix validated by rebuilding only dm.ko (make in sys/dev/disk/dm),
installing to /boot/kernel/dm.ko, kldunload/kldload dm, re-running
the same PoC.
Fix verification
fixedVALIDATED: PoC panics on unpatched #0 (Stopped at dev_dioctl+0xc, fault va=0xa8). Fixed dm.ko: same PoC returns reload rv=2 (ENOENT) cleanly, no panic, guest up.
BEFORE (unpatched #0): Fatal trap 12, fault va=0xa8, Stopped at dev_dioctl+0xc: movq 0xa8(%rdi),%rax. AFTER (fixed dm.ko): reload rv=2 (No such file or directory) -- kernel survived.
Confirmed kernel references
Detail
Exploit chain
none β valid hard blocker (root-only reachability): /dev/mapper/control 0640 root:operator, maxx not in operator/wheel. NULL-deref no write primitive, no escalation. root->kernel DoS.
Evidence (decisive lines)
Fatal trap 12: page fault while in kernel mode | fault virtual address = 0xa8 | Stopped at dev_dioctl+0xc: movq 0xa8(%rdi),%rax | current process = 864 (poc). Guest down after reload ioctl with regular-file backing path.
PoC changes
Rewrote poc.c: finding's described trigger (/dev/nonexistent) was false positive (handled cleanly by linear target NULL check dm_target_linear.c:67). Discovered actual trigger is REGULAR FILE as backing path β vn_open succeeds, v_rdev NULL, dev_dioctl crashes. Updated PoC to create /tmp/df2450_backing and use as linear backing path. fix.diff: check v_rdev != NULL before dev_dioctl in dm_pdev_insert.
Verified recommended fix
In dm_pdev.c dm_dev_insert, after dm_dk_lookup succeeds (line 151) and before dm_pdev_get_vattr (line 153), add: if (dmp->pdev_vnode->v_rdev == NULL) { dm_pdev_free(dmp); lockmgr release; return NULL; }. Rejects non-device vnodes (regular files) which have NULL v_rdev. Caller (linear init) already checks for NULL and returns ENOENT.
Verdict
REPRODUCED. dm_pdev_insert (dm_pdev.c:168) calls dev_dioctl(dmp->pdev_vnode->v_rdev, DIOCGPART, ...) without checking v_rdev != NULL. When backing path resolves to a NON-BLOCK-DEVICE vnode (regular file, e.g. /tmp/df2450_backing), vn_open succeeds but v_rdev is NULL, so dev_dioctl dereferences NULL+0xa8 -> Fatal trap 12 page fault. The finding's described trigger (/dev/nonexistent) is handled cleanly (dm_dk_lookup fails -> NULL -> linear ENOENT). Actual trigger is a regular file as backing path. Underlying bug (NULL v_rdev in dm_pdev.c:168) real. Confirmed: 'Stopped at dev_dioctl+0xc: movq 0xa8(%rdi),%rax'.
No comments yet.