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

dm unit allocation is unbounded while the dev_t minor encoding holds only 512 units β€” the 513th live dm device aliases a live device's minor and KKASSERT-panics dm_dev_insert deterministically (race-free); dkunit() truncation on destroy also frees live devices' clone-bitmap bits

Summary

devfs_clone_bitmap_get is called with limit=0 (unlimited) but dkmakeminor encodes only 9 unit bits (DKMAXUNITS=512) - the 513th live unit aliases a live minor, the insert's by-minor collision lookup hits the KKASSERT (same site as DF-2442 but a DIFFERENT root cause: sequential, race-free), and on non-assert builds every by-minor ioctl operates on the wrong device while destroy's dkunit truncation frees the aliased live device's bit. REPRODUCED on guest: 512 creates OK (minors exactly as predicted), 513th panics deterministically; fix (limit to DKMAXUNITS-1 + mask the recycle) validated by in-guest module rebuild - clean refusal, guest survives. Operator-group; realistic on laden LVM boxes.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4900 Β· 16 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
poc_phase1.sh β€” 819 B view raw
poc_phase2.sh β€” 476 B view raw
dm_minor_alias.sh β€” 1.6 KB view raw
fix_test.sh β€” 825 B view raw
build.log β€” 1.0 KB view raw
run.log β€” 323 B view raw
fix_run.log β€” 328 B view raw
panic.txt β€” 758 B view raw
panic.2.txt β€” 685 B view raw
panic.accidental-run.txt β€” 758 B view raw
fix.diff β€” 1.0 KB view raw
verdict.json β€” 3.7 KB view raw
manifest.json β€” 1.5 KB view raw
env.txt β€” 515 B view raw

DF-4900 β€” dm minor-number unit aliasing: 513th device panics the kernel (deterministic)

File: sys/dev/disk/dm/dm_dev.c

devfs_clone_bitmap_get(&dm_minor_bitmap, 0) (dm_dev.c:234) hands out dm unit numbers without any bound, but the dev_t minor encoding (dkmakeminor, sys/sys/diskslice.h:248-258 β€” unit bits 3-7 and 21-24, i.e. 9 bits = DKMAXUNITS = 512) cannot represent units β‰₯ 512: disk_create_named(name, unit, …) β†’ make_only_dev(…, dkmakewholedisk(unit), …) (subr_disk.c:700) and dmv->minor = minor(dmv->devt) (dm_dev.c:261).

From the 513th simultaneously-allocated unit on, the new device's minor aliases (mod 512) the minor of a live dm device. dm_dev_insert then looks up _dm_dev_lookup(dev->name, NULL, dev->minor) (dm_dev.c:191) β€” the by-minor walk (dm_dev.c:129-131) HITS the live device, the else-branch is taken with dmv == NULL, and KKASSERT(dmv != NULL) (dm_dev.c:195) panics. Same assert site as DF-2442, but race-free: 513 sequential dmsetup create <uniq> --notable ioctls, no concurrency needed.

Secondary consequences (post-assert / on builds without KKASSERT): - dkunit() (diskslice.h:318-324) decodes only the 9-bit unit field, so destroying a unit-β‰₯512 device feeds the aliased live device's unit to devfs_clone_bitmap_put (dm_dev.c:285,290) β€” the live device's clone bit is freed and can be double-allocated. - duplicate live minors β†’ every by-minor path (dmopen/dmclose is_open tracking, dm_dev_remove/status/suspend/resume/table ioctls) operates on the FIRST device in the TAILQ, not the one addressed.

Reproduce (as run on the audit guest)

scp poc_phase1.sh poc_phase2.sh dfbsd:/root/
ssh dfbsd 'sh /root/poc_phase1.sh'   # 512 devices: all succeed, minors distinct
ssh dfbsd 'sh /root/poc_phase2.sh'   # 513th unit -> kernel panic

Expected: phase 1 prints xa/ya with the SAME recycled minor (0x1e110007) and phase1 OK; phase 2 panics: panic: assertion "dmv != NULL" failed in dm_dev_insert at .../dm_dev.c:195 (panic.2.txt; first accidental observation in panic.accidental-run.txt).

Fix validation

fix.diff bounds the allocation (devfs_clone_bitmap_get(&dm_minor_bitmap, DKMAXUNITS - 1), ENOSPC rollback) and masks the put (minor & (DKMAXUNITS-1))). Module-only rebuild in the guest (build.log): baseline panics on the 513th create; patched module refuses it cleanly (513th create refused) and the guest stays up (fix_run.log).

VERDICT.md
↓ download raw

DF-4900 VERDICT β€” reproduced (deterministic kernel panic), fix validated

What was observed (audit guest, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC)

  1. Baseline, controlled run (run.log + panic.2.txt): - Part A: dmsetup create xa --notable β†’ minor 33,0x1e110007 (= dkmakewholedisk(0) = slice 1 + part 0xFF bits + unit 0 β€” predicted exactly from diskslice.h before running); remove xa; create ya β†’ the SAME minor β€” clone bitmap round-trip is exact below the 512-unit boundary. - Part B: 512 sequential creates (units 0..511) all succeed with distinct minors (d0511 = 0x1ff100ff = dkmakewholedisk(511)). - Phase 2: with those 512 live, the next create allocates unit 512, whose encoded minor is identical to the live unit-0 device's (dkmakeminor unit field is 9 bits: (512&0x1f)=0, (512&0x1e0)=0). dm_dev_insert (dm_dev.c:191) walks by minor, hits the live device, falls into the else-branch with dmv==NULL, and KKASSERT(dmv != NULL) at dm_dev.c:195 fires: panic: assertion "dmv != NULL" failed in dm_dev_insert at /usr/src/sys/dev/disk/dm/dm_dev.c:195 backtrace: dm_dev_insert ← dm_dev_create ← dm_dev_create_ioctl ← dmioctl ← dev_dioctl. Guest wedged in DDB (impact = panic/DoS).
  2. Fix validation (build.log, fix_run.log): dm.ko rebuilt in-guest with fix.diff (make in /usr/src/sys/dev/disk/dm). With the patched module: 512 creates succeed; the 513th is refused cleanly (513th create refused: 1), uptime shows the guest alive, dmsetup remove_all succeeds. Bad behavior gone.

Root cause chain (path:line)

  • dm_dev.c:234 β€” devfs_clone_bitmap_get(&dm_minor_bitmap, 0): the second argument is the limit; 0 means "no limit" (devfs_helper.c:221-236 only refuses when limit > 0 && unit > limit). The bitmap extends without bound (devfs_helper.c:78-92), so unit 512+ is handed out.
  • diskslice.h:248-258 β€” dkmakeminor encodes unit bits 0-8 only ((unit&0x1f)<<3 | (unit&0x1e0)<<16); DKMAXUNITS is 512 (diskslice.h:107). Unit 512 encodes identically to unit 0.
  • subr_disk.c:700 + dm_dev.c:252,261 β€” the cdev is created with dkmakewholedisk(unit) and dmv->minor = minor(devt).
  • dm_dev.c:191-195 β€” insert collision check _dm_dev_lookup(name,NULL,minor) matches the live device by MINOR; else-branch KKASSERTs on dmv != NULL (uuid lookup was skipped because the new uuid is zero β€” dm_dev.c:187).
  • dm_dev.c:285,290 β€” on destroy of a unitβ‰₯512 device, dkunit(dmv->devt) (diskslice.h:318-324) truncates to 9 bits, so devfs_clone_bitmap_put frees the ALIASED LIVE device's unit bit (double-allocation feeder).

Impact / threat

  • Precondition: open /dev/mapper/control (0640 root:operator β€” the same privilege bar as the filed dm family, DF-2442/2443/2444).
  • Deterministic local kernel panic (DoS) β€” no race, no concurrency: 513 sequential create ioctls (observed in <10 s of dmsetup looping; the 512-device setup phase takes a few minutes).
  • On builds without KKASSERT, or if the assert were replaced by EEXIST handling, the same allocation overflow yields duplicate live device numbers: dm_dev_lookup_minor (dm_dev.c:124-135) returns the first TAILQ match, so by-minor open-tracking (dmopen/dmclose set is_open on the wrong device, device-mapper.c:205-239) and by-minor ioctls (remove/status/suspend/resume/table) operate on the wrong device β€” wrong-device teardown/data-path reconfiguration, plus clone-bit double-allocation via the dkunit truncation.

Relation to known findings (not a re-report)

  • DF-2442 (known) is the SAME assert site but a DIFFERENT trigger: a concurrent NAME collision between two racing creates. Here the trigger is deterministic unit-space exhaustion; a single-threaded operator hits it, and the fix is in the allocator bound, not in insert's collision handling. The known list was honored: DF-1959/2442/2443/2444/2445 shapes were not re-derived.

Fix

fix.diff (validated): bound allocation to the encodable unit space and return ENOSPC with rollback; mask the put value defensively.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest's /usr/src copy, dm.ko rebuilt (build.log) and loaded: baseline kernel panics on the 513th create (panic.2.txt); patched module refuses it cleanly with an error, guest stays up, remove_all succeeds (fix_run.log). Bad behavior GONE.

findings/poc/DF-4900/build.log, findings/poc/DF-4900/fix_run.log, findings/poc/DF-4900/fix.diff
↓ fix.diffper-fix-DF-4900

Confirmed kernel references

Detail

Exploit chain

operator/root opens /dev/mapper/control (0640 root:operator) -> 513 sequential DM_DEV_CREATE ioctls with unique names -> unit 512 allocated (clone bitmap unbounded) -> dkmakewholedisk(512) == dkmakewholedisk(0) -> dm_dev_insert by-minor lookup hits the live unit-0 device -> KKASSERT panic (deterministic DoS). On non-assert builds: duplicate live minors -> dm_dev_lookup_minor returns first TAILQ match -> by-minor open-tracking and remove/status/suspend/table ioctls operate on the WRONG device; destroy of a unit>=512 device feeds dkunit() truncation (dm_dev.c:290) to devfs_clone_bitmap_put, freeing the aliased live device's unit bit (double-allocation).

Evidence (decisive lines)

run.log (Part A recycle + 512 live devices), panic.2.txt (controlled panic with DF4900 console markers; first observation in panic.accidental-run.txt), fix_run.log + build.log (patched module: 513th refused, guest alive), fix.diff

PoC changes

no seed PoC; authored from scratch after deriving the exact minor encoding (predicted 0x1e110007 for unit 0 from diskslice.h before running; guest matched)

Verified recommended fix

Bound the clone-bitmap allocation to the encodable unit space: devfs_clone_bitmap_get(&dm_minor_bitmap, DKMAXUNITS-1) with ENOSPC rollback in dm_dev_create, and mask the put value in dm_dev_destroy.

Verdict

Reproduced deterministically on the audit guest: 512 sequential dmsetup creates succeed (distinct minors, exact clone-bitmap recycle verified in run.log Part A); the 513th create allocates unit 512 whose dkmakeminor encoding aliases the live unit-0 device's minor, so dm_dev_insert (dm_dev.c:191) matches by minor and KKASSERT(dmv != NULL) at dm_dev.c:195 panics the kernel (panic.2.txt, backtrace through dm_dev_create_ioctl/dmioctl). Root cause: devfs_clone_bitmap_get(&dm_minor_bitmap, 0) at dm_dev.c:234 passes limit=0 (unlimited) while the minor encoding (diskslice.h:248, DKMAXUNITS=512) holds 9 unit bits. Fix validated by in-guest module rebuild (build.log): with fix.diff the 513th create is refused cleanly and the guest stays up (fix_run.log).