DF-5151 / verdict.json
{ "finding_id": "DF-5151", "status": "reproduced", "reproduced": 1, "impact": "none", "confidence": "certain", "verdict": "Deterministically reproduced on the stock 6.5-DEVELOPMENT guest (two boots, incl. fresh vm.sh reset with-src): dm_pdev_alloc strlcpys 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 callers 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 Ls 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/<serial>-part*.s1d) exceed 31 chars and same-model disks share prefixes. Fix validated: with the strlen bounds check in dm_pdev_insert, the 33-char path load is loudly rejected and the 31-char path binds to its own correct device.", "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 <L> 0 where L is a >=32-char path to device X (kernel stores truncated 31-char key T), then reloads another device with params <T> 0 where T resolves to device Y (or is nonexistent) -> second table silently binds to X; all IO intended for Y lands on X. No unprivileged route: pdev vn_open runs under caller creds.", "evidence": "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.", "kernel_refs": [ "sys/dev/disk/dm/dm_pdev.c:204", "sys/dev/disk/dm/dm_pdev.c:129", "sys/dev/disk/dm/dm_pdev.c:60", "sys/dev/disk/dm/linear/dm_target_linear.c:67", "sys/dev/disk/dm/linear/dm_target_linear.c:114", "sys/dev/disk/dm/dm.h:62", "sys/dev/disk/dm/dm.h:114", "sys/dev/disk/dm/dm_ioctl.c:771" ], "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 <plist>...</plist> - without it copyin fails EIO; (2) tables load into the INACTIVE slot - a resume ioctl is required before deps shows anything (first comparison compared two empty arrays); deps parser tightened to only read integers inside the cmd_data array.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 990, "guest_dirty": 0, "build_cmd": "cd /tmp/poc5151 && cc -O2 -Wall -o poc_dm_pdev_trunc poc_dm_pdev_trunc.c", "run_cmd": "cd /tmp/poc5151 && sh run.sh (baseline: kldload dm; kldload dm_target_linear; ./poc_dm_pdev_trunc 0x<md0 dev_t>; fixed: DM_KO=/usr/obj/usr/src/sys/dev/disk/dm/dm.ko sh run.sh)", "code_hash": "9c12f34834b45a822fd206a29137bab15a9fd823e2cfafbfeb368c04a39c672c", "notes": "Deterministic logic bug, no race. Wrong-device binding persists for the life of the table; IO for the mis-bound volume silently lands on another disk (integrity + potential cross-volume exposure within roots config). Duplicate-pdev side effect: two long paths sharing a 31-char prefix both miss each other and create parallel pdev entries under the same truncated key. Do-not-re-report honored: DF-2150 (offset/length bounds), DF-2058/DF-2451 (dump div0), DF-2057/DF-2450 (non-device vnode), DF-4922 (init-failure pdev leak class).", "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.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "dm.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_evidence": "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" } |