dm pdev dedup key silently truncated to 31 chars: a linear table load binds to the wrong physical device (or to a device whose path was never opened)
Summary
The 32-byte name field IS the dedup key, strlcpy truncates silently - a later path matching the truncated prefix strcmp-matches and reuses the earlier pdev, binding the table to the WRONG disk without opening the requested path. VERIFIED on guest: a table intending /dev/md0 came back bound to /dev/md0s0 (deps dev 0x1e100007 vs 0x1e110007), deterministic across two boots; serno-style DFly paths routinely exceed 31 chars and same-model disks share prefixes. Reject-over-length fix validated on rebuilt dm.ko (loud failure + correct binding).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5151 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_dm_pdev_trunc.c | β | 8.6 KB | view raw | |
| build.sh | β | 127 B | view raw | |
| run.sh | β | 179 B | view raw | |
| run.log | β | 6.8 KB | view raw | |
| run.2.log | β | 6.8 KB | view raw | |
| fix.diff | β | 989 B | view raw | |
| fix_build.log | β | 48.3 KB | view raw | |
| fix_run.log | β | 5.0 KB | view raw | |
| env.txt | β | 288 B | view raw | |
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| verdict.json | β | 5.0 KB | view raw |
DF-5151 β dm pdev dedup key silently truncated to 31 chars β wrong-device table binding
- File audited (pass 2):
sys/dev/disk/dm/linear/dm_target_linear.c(root cause insys/dev/disk/dm/dm_pdev.c, reached through the linear target's unvalidatedargv[0]device path). - Severity: Low (privileged local β table loads go through
NETBSD_DM_IOCTLon/dev/mapper/control, 0640 root:operator; the group cannot usefully drive it because the pdevvn_open(FREAD|FWRITE)runs under the caller's credentials). - Verdict: reproduced on the stock 6.5-DEVELOPMENT guest (two independent
boots, incl. a fresh
vm.sh reset with-src). Fix validated on a rebuiltdm.ko.
Build
./build.sh # cc -O2 -Wall -o poc_dm_pdev_trunc poc_dm_pdev_trunc.c
(no libprop needed β the plist XML is hand-built; only <sys/ioctl.h>)
Run (as root on the guest)
sh run.sh # baseline: kldload dm; kldload dm_target_linear
Setup created by run.sh:
T = /tmp/dmtr/aaaaaaaaaaaaaaaaaaaaa (exactly 31 chars) -> symlink to /dev/md0 (Y) L = /tmp/dmtr/aaaaaaaaaaaaaaaaaaaaabb (33 chars) -> symlink to /dev/md0s0 (X)
What the PoC does:
createdm devices dfa/dfb/dfcreloaddfa with linear params "<L> 0" β kernel opens X (md0s0) and stores the pdev understrlcpy(name, L, 32)= T (truncated)reloaddfb with linear params "<T> 0" β T resolves to Y (/dev/md0), butdm_pdev_lookup_name(T)strcmp-matches the truncated stored key β dfb silently reuses dfa's pdev without ever opening T- control:
reloaddfc with a nonexistent path β must FAIL (normal behavior) resumedfa/dfb (tables load into the inactive slot), thendepsboth
Expected output (stock kernel β the bug)
expected deps(dfb) if correct = 0x1e110007 <- dev_t of /dev/md0 reload dfc (...): ioctl FAILED with errno=2 <- control behaves normally ==== RESULT ==== deps(dfa [path L -> /dev/md0s0]) = 0x1e100007 <- md0s0, correct for dfa deps(dfb [path T -> /dev/md0 ]) = 0x1e100007 <- WRONG: dfb got md0s0's pdev! MISDIRECTION PROVEN (BUG PRESENT): ... poc exit=0
/dev/md0 is 0x1e110007, /dev/md0s0 is 0x1e100007 (see ls -l in run.log).
Every IO issued to dm device dfb lands on /dev/md0s0 even though its table was
loaded with a path that resolves to /dev/md0.
Expected output (patched dm.ko β fix validated)
reload dfa (/tmp/dmtr/aaaaaaaaaaaaaaaaaaaaabb): ioctl FAILED with errno=2 dfa reload with 33-char path was REJECTED by kernel deps(dfb [path T -> /dev/md0 ]) = 0x1e110007 <- correct device now FIX CONFIRMED: long path rejected loudly, short path bound to its own device
Console/dmesg evidence of the loud rejection:
dm_pdev_insert: device path too long (max 31): /tmp/dmtr/aaaaaaaaaaaaaaaaaaaaabb
Fix rebuild + run:
cd /usr/src && patch -p1 < /tmp/poc5151/fix.diff cd /usr/src/sys/dev/disk/dm && make obj && make # -> dm.ko (-Werror clean) cd /tmp/poc5151 && DM_KO=/usr/obj/usr/src/sys/dev/disk/dm/dm.ko sh run.sh
Artifacts
poc_dm_pdev_trunc.cβ PoC source (hand-built proplist XML ioctls)build.sh,run.shβ exact commandsrun.logβ decisive baseline run (dirty-boot guest)run.2.logβ baseline run on a FRESHvm.sh reset with-srcboot (deterministic)fix.diffβ git-apply-able fix (bounds check indm_pdev_insert)fix_build.log,fix_run.logβ patched-module build + verification runenv.txtβ guest uname + device list
DF-5151 VERDICT β dm_pdev dedup key truncation β wrong-device table binding
Status: reproduced (deterministic, two boots). Fix validated on rebuilt dm.ko.
Impact: silent cross-device data misdirection for dm-linear (and any target
that passes device paths to dm_pdev_insert). No memory corruption.
Root cause (line-accurate)
sys/dev/disk/dm/dm.h:62β#define DM_MAX_DEV_NAME 32; the pdev is keyed bychar name[DM_MAX_DEV_NAME](sys/dev/disk/dm/dm.h:114).sys/dev/disk/dm/linear/dm_target_linear.c:67βdm_target_linear_initpasses the user-suppliedargv[0](fromDM_TABLE_PARAMS,sys/dev/disk/dm/dm_ioctl.c:771-772β no length validation anywhere on the path;dm_table_init's strsep at dm_ioctl.c:826-830 imposes no bound) intodm_pdev_insert(argv[0]).sys/dev/disk/dm/dm_pdev.c:203-204βdm_pdev_allocdoesstrlcpy(dmp->name, name, DM_MAX_DEV_NAME): a path longer than 31 chars is silently truncated.sys/dev/disk/dm/dm_pdev.c:60-63+128-136β dedup isstrcmpof the caller's FULL path against the STORED (possibly truncated) name. Consequences: 1. A laterdm_pdev_insert(T)whereTequals the 31-char truncated prefix of an earlier long pathLstrcmp-matches β the new table entry silently reuses L's pdev (L's physical device).vn_open(T)never happens β T doesn't even have to exist. 2. Two distinct long paths sharing a 31-char prefix both miss each other (full β truncated) β duplicate pdev entries under the same truncated key (refcount/vnode churn, unbounded pdev duplication under table reloads).sys/dev/disk/dm/dm_target_linear.c:114-116β strategy then routes every bio for the mis-bound table totlc->pdev's vnode attlc->offset: reads/writes land on the wrong physical device.
Reachability: NETBSD_DM_IOCTL on /dev/mapper/control
(sys/dev/disk/dm/device-mapper.c:181, 0640 root:operator). ioctls are not
flag-gated, but the pdev vn_open(FREAD|FWRITE) in dm_dk_lookup
(dm_pdev.c:74-78) runs under the caller's credentials, so an operator-group
user cannot open real disk nodes RW β effectively root-only to reach a
meaningful device. Severity Low accordingly.
Reproduction (see run.log / run.2.log)
- T =
/tmp/dmtr/aaaaaaaaaaaaaaaaaaaaa(31 chars) β symlink β/dev/md0(0x1e110007) - L =
/tmp/dmtr/aaaaaaaaaaaaaaaaaaaaabb(33 chars) β symlink β/dev/md0s0(0x1e100007) - reload dfa with
L 0(linear): stored pdev name := first 31 chars of L == T. - reload dfb with
T 0(linear): user intends /dev/md0; kernel strcmp-matches the truncated key and hands back dfa's pdev. - deps:
deps(dfa) == deps(dfb) == 0x1e100007(md0s0) although dfb's path resolves to md0 (0x1e110007). Control reload with a non-colliding nonexistent path fails ENOENT, i.e. the kernel normally rejects unknown paths β the collision is what bypasses the open entirely. - Deterministic: identical result on a fresh
vm.sh reset with-srcboot.
Real-world shape: DragonFly-style long device paths (/dev/serno/<long
serial>-part*.s1d easily exceed 31 chars) are exactly the class that
truncates; any two same-model disks whose serials agree in the first ~20
characters collapse onto one truncated key.
Fix validation (fix.diff, fix_build.log, fix_run.log)
dm_pdev_insert now rejects strlen(dev_name) >= DM_MAX_DEV_NAME with a loud
kprintf (dm.ko rebuilt in-dir with -Werror, clean). Re-running the exact
PoC against the patched module:
reload dfa(33-char path) β rejected (ENOENT to userland; console:dm_pdev_insert: device path too long (max 31): /tmp/dmtr/...)reload dfb(31-char T) β accepted,deps(dfb) = 0x1e110007= /dev/md0 β the correct device.- No misdirection possible: the truncating insert can no longer happen.
Alternative (bigger) upstream fix: enlarge name[]/key pdevs by dev_t instead
of by name string; the bounds check is the minimal safe change and turns silent
misconfiguration into a loud failure.
Not-this-bug (checked and killed)
- table/strategy/destroy lifetime: readers hold
table_mtxSHARED across the whole dispatch (dm_table.c:65-74, device-mapper.c:408-465); switch/destroy take EXCLUSIVE (dm_table.c:118,136) β no linear UAF window; module unload is refused while tables hold target refs (dm_target.c:181-184). dm_target_linear_tableNULL-config deref: unreachable β entries only enter a table after init success, and failed loads destroy the partial table (dm_ioctl.c:783-790).kmalloc(M_WAITOK)==NULLbranch (dm_target_linear.c:70-72) is dead code; the pdev-ref leak it would cause mirrors known DF-4922 (flakey).- dump-offset div-by-zero when DIOCGPART fails: known DF-2058/DF-2451.
- non-device vnode backing (v_rdev NULL): known DF-2057/DF-2450.
- offset/length bounds +
atoi64wrap (feedstlc->offset * DEV_BSIZEat dm_target_linear.c:114): known DF-2150, not re-reported. upcall: no such member in DragonFly'sdm_target_t(dm.h:159-175) β dead NetBSD-ism; nothing to audit.dm_table_add_depsreturn ignored (dm_target_linear.c:77): linear uses exactly one pdev per entry, the -1 duplicate case cannot arise for this target.
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
fixeddm.ko rebuilt in-dir from patched /usr/src (-Werror clean, fix_build.log). Baseline on stock module: misdirection reproduced (deps(dfb)=0x1e100007, wrong device). Patched module: 33-char path load rejected loudly (console: 'dm_pdev_insert: device path too long (max 31)'), 31-char path binds correctly (deps(dfb)=0x1e110007=/dev/md0), 'FIX CONFIRMED'. Bad behavior gone.
fix_run.log (patched-module run output), fix_build.log (patch -p1 + make obj + make, dm.ko), dmesg line 'dm_pdev_insert: device path too long (max 31): /tmp/dmtr/aaaaaaaaaaaaaaaaaaaaabb'
Confirmed kernel references
Detail
Exploit chain
root (or any credential able to open the backing devices RW) sends NETBSD_DM_IOCTL reload to /dev/mapper/control with linear params '
Evidence (decisive lines)
run.log / run.2.log: 'deps(dfb [path T -> /dev/md0]) = 0x1e100007' vs 'expected ... = 0x1e110007' + 'MISDIRECTION PROVEN'; control reload of a non-colliding nonexistent path fails ENOENT (normal rejection bypassed only via the truncated-key collision). fix_run.log: 'FIX CONFIRMED ... deps(dfb) = 0x1e110007' and console 'dm_pdev_insert: device path too long (max 31)' in dmesg.
PoC changes
Seed sketch had to be written from scratch (no libdevmapper on guest, no proplib headers): hand-built proplist XML over NETBSD_DM_IOCTL with a locally-defined struct plistref {void*,size_t}. Two iterations to correct behavior: (1) proplib internalizer requires the document wrapped in
Verified recommended fix
Reject device paths >= DM_MAX_DEV_NAME in dm_pdev_insert (loud EINVAL path) or enlarge/dynamically allocate the pdev name and key pdevs by dev_t instead of by name string.
Verdict
Deterministically reproduced on the stock 6.5-DEVELOPMENT guest (two boots, incl. fresh vm.sh reset with-src): dm_pdev_alloc strlcpy's the table-load device path into the fixed 32-byte pdev name key (dm_pdev.c:204) and dm_pdev_insert dedups by strcmp of the caller's full path against that stored, silently-truncated key (dm_pdev.c:129,60-63). A table loaded with a >=32-char path L stores key T=first-31-chars(L); a subsequent linear table loaded with path T (which may resolve to a completely different device, or not exist at all) strcmp-matches T and silently reuses L's pdev without ever opening T. deps() proves the misbinding: a table whose path resolves to /dev/md0 (0x1e110007) came back bound to /dev/md0s0 (0x1e100007). Every bio for the mis-bound device is routed to the wrong physical disk (dm_target_linear.c:114-116). No kernel memory corruption: impact class is silent privileged data misdirection (cross-device IO routing), so impact=none in the corruption-oriented enum; long real-world paths (/dev/serno/
No comments yet.