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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2450 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run instructions
↓ download 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.

VERDICT.md verdict full analysis
↓ download raw

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)

  1. Attacker opens /dev/mapper/control (0640 root:operator β€” root/operator only) and sends create + reload ioctls (libprop NETBSD_DM_IOCTL).
  2. The reload specifies target type linear with params <path> 0 where <path> is a regular file (e.g. /tmp/df2450_backing).
  3. dm_table_load_ioctl (dm_ioctl.c:783) calls dm_target_linear_init (dm_target_linear.c:56).
  4. dm_target_linear_init calls dm_pdev_insert(argv[0]) (line 67).
  5. dm_pdev_insert (dm_pdev.c:120) calls dm_dk_lookup (line 143) β†’ vn_open succeeds for the regular file.
  6. dm_pdev_insert then calls line 168: c error = dev_dioctl(dmp->pdev_vnode->v_rdev, DIOCGPART, ...); For a regular-file vnode, v_rdev is NULL. dev_dioctl(NULL, ...) reads 0xa8(%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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 with fixed dm.ko (SHA256 f07dfdf4e480dd45d6e87fdbd270e8080dc47bc827199230c339e09db304f1c1)

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'.