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)
PoC verification
Evidence pack
findings/poc/DF-4900 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
DF-4900 VERDICT β reproduced (deterministic kernel panic), fix validated
What was observed (audit guest, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC)
- Baseline, controlled run (run.log + panic.2.txt):
- Part A:
dmsetup create xa --notableβ minor33,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 (dkmakeminorunit 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 withdmv==NULL, andKKASSERT(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:195backtrace: dm_dev_insert β dm_dev_create β dm_dev_create_ioctl β dmioctl β dev_dioctl. Guest wedged in DDB (impact = panic/DoS). - Fix validation (build.log, fix_run.log): dm.ko rebuilt in-guest with
fix.diff (
makein /usr/src/sys/dev/disk/dm). With the patched module: 512 creates succeed; the 513th is refused cleanly (513th create refused: 1),uptimeshows the guest alive,dmsetup remove_allsucceeds. 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 whenlimit > 0 && unit > limit). The bitmap extends without bound (devfs_helper.c:78-92), so unit 512+ is handed out. - diskslice.h:248-258 β
dkmakeminorencodes unit bits 0-8 only ((unit&0x1f)<<3 | (unit&0x1e0)<<16);DKMAXUNITSis 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)anddmv->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 ondmv != 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, sodevfs_clone_bitmap_putfrees 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/dmclosesetis_openon 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
fixedfix.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
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).
No comments yet.