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

Drive free list grows without bound: OOB write past the 16-entry (256-byte) drive->freelist heap array with attacker-chosen u64 pairs from ordinary config

Summary

drive->freelist is allocated once with 16 entries; freelist_size is never referenced again. give_sd_to_drive and return_drive_space increment freelist_entries and write freelist[fe(+1)] = {u64 offset, u64 sectors} with no capacity check. Every fragmented subdisk placement adds one 16-byte entry past the 16th. LIVE-REPRODUCED: 60 one-sector sds at 2-sector pitch -> freelist grows to ~61 in a 16-slot array; after the next zone reuse: Fatal trap 12 with fault address 0x163 == exactly the attacker-written sdend - the overflow content lands in live adjacent objects and is dereferenced as a pointer. EXPAND-if-full fix (the field already exists for exactly this purpose).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4140 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
panic.txt β€” 986 B view raw
serial_full.txt β€” 3.3 KB view raw
cfgfrag2.sec β€” 3.5 KB ↓ download
verdict.json β€” 2.8 KB view raw

DF-4140 β€” vinum drive free-list grows without bound: OOB write past the

16-entry drive->freelist heap array with attacker-chosen u64 pairs

Impact

Unbounded kernel heap buffer overflow (16 bytes per extra entry, unlimited entry count, content = two attacker-influenced u_int64_ts) from ordinary vinum configuration operations β€” placing subdisks at specific driveoffsets, or returning fragmented space. Reproduced on the guest: the overflow value (0x163 = one of our subdisk end offsets) was later dereferenced as a kernel pointer by prop_object_release β†’ page fault.

Root cause

  • sys/dev/raid/vinum/vinumio.c:143-145 β€” the free list is allocated once with INITIAL_DRIVE_FREELIST = 16 entries (vinumvar.h:191) = Malloc(16 * sizeof(struct drive_freelist)) = 256 bytes. drive->freelist_size is set to 16 and never referenced again anywhere in the driver (no growth, no capacity check).
  • sys/dev/raid/vinum/vinumconfig.c:429-439 (give_sd_to_drive, "specific offset" case 3) β€” placing a subdisk strictly inside a free entry splits it and does drive->freelist_entries++; drive->freelist[fe+1].offset = sdend; drive->freelist[fe+1].sectors = dend - sdend; with no bound.
  • sys/dev/raid/vinum/vinumconfig.c:696-705 (return_drive_space, case 3) β€” returning non-contiguous space inserts an entry the same way.

Every fragmented placement adds one 16-byte entry past the 16th. sdend is fully attacker-chosen (the driveoffset + len from the config line); sectors is derived. 17th placement = first OOB write; N placements β‰ˆ 16Β·(Nβˆ’16) bytes past a kmalloc(256).

Reachability

  • root/operator vinum create with multiple sd ... driveoffset Xs len Ys detached lines at interior offsets (exactly how the PoC drives it),
  • any crafted vinum-labelled disk β€” the identical config_subdisk β†’ give_sd_to_drive path runs in VF_READING_CONFIG mode,
  • space return paths (remove sd, vinum move) fragment via return_drive_space the same way.

Build / run

cfgfrag2.sec (60 one-sector sds at 2-sector pitch) goes to the vinum config area of a crafted drive (see DF-4133 pack for label crafting), then:

vinum read /dev/vn0s1a        # freelist_entries: 1 -> ~61 in a 16-slot array
# repeat read after VINUM_RESETCONFIG + relabel to consume the smashed zone:
./vreset; dd if=lbl.bin of=/dev/vn0s1a bs=512 seek=8 conv=notrunc
vinum read /dev/vn0s1a        # -> panic (see panic.txt)

Expected (vulnerable kernel)

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x163                       <- our sdend value (300+2*27+1)
Stopped at prop_object_release+0x3d: movq (%rbx),%rax

(the first read usually survives β€” the corruption is dormant until the 256-byte zone object adjacent to freelist[] is used; a second cycle crashes deterministically in this run.)

Fix

Grow the array when entries reach capacity (the field for it already exists):

--- a/sys/dev/raid/vinum/vinumconfig.c
+++ b/sys/dev/raid/vinum/vinumconfig.c
@@ -429,6 +429,15 @@
        } else {                    /* 3 or 4 */
            drive->freelist[fe].sectors = sd->driveoffset - drive->freelist[fe].offset;
            if (dend > sdend) {             /* 3: space at the end as well */
+           if (drive->freelist_entries >= drive->freelist_size) {
+               EXPAND(drive->freelist, struct drive_freelist,
+                  drive->freelist_size, INITIAL_DRIVE_FREELIST);
+               if (drive->freelist == NULL)
+               return;
+           }
            if (fe < (drive->freelist_entries - 1)) /* not the last one */

(the same guard is needed in return_drive_space before the insert at :696-705.)

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Fix not built in this run (fix-validation kernel carried DF-4132/4133 only); proposed EXPAND guard in README.

panic.txt baseline.
per-fix-DF-4140

Confirmed kernel references

Detail

Exploit chain

config with N fragmented sd placements -> freelist_entries grows 1->N+1 in a 16-slot array -> 16-byte OOB writes of chosen u64 pairs past kmalloc(256) -> adjacent 256-byte zone objects (e.g. libprop objects) corrupted with small-integer 'pointers' -> crash on use (0x163 deref). Ceiling: controlled heap corruption; the written offsets/lengths are config-chosen u64s.

Evidence (decisive lines)

panic.txt (fault VA == our sdend value), serial_full.txt, cfgfrag2.sec, README.md

PoC changes

Used space-separated kernel config grammar; sizespec suffix s=sectors (first attempt with len 512b created 512-sector sds that overlapped); crash needed a second resetconfig+read cycle to consume the corrupted zone.

Verified recommended fix

EXPAND drive->freelist when freelist_entries reaches freelist_size before both insertion sites (field already exists for this).

Verdict

drive->freelist is allocated once with 16 entries (256 bytes, vinumio.c:143-145, INITIAL_DRIVE_FREELIST=16) and freelist_size is never checked again; give_sd_to_drive case 3 (vinumconfig.c:429-439) and return_drive_space case 3 (:696-705) increment freelist_entries and write 16-byte {u64 offset, u64 sectors} entries with no bound. Reproduced: a crafted config placing 60 one-sector subdisks at 2-sector pitch grows the list to ~61 entries (~720 bytes past the 256-byte kmalloc); after the following zone reuse the kernel dereferenced one of the attacker-written values (fault VA 0x163 == sdend of sd f27) in prop_object_release -> Fatal trap 12. Root/operator config path and crafted-disk read path both reach it.