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

vinum volume-minor aliasing: volume #256 wraps into VOL[0] β€” every access to /dev/vinum/vol/v256 is served by volume v0 (deterministic cross-volume data corruption + wrong-object sizing)

Summary

VINUM_VOL_WIDTH is 8 bits but nothing bounds the volume count: v256 gets minor 0x100 which Volno() decodes as volno 0 - open/read/write/ioctl/DIOCGPART all execute against VOL[0]. REPRODUCED on guest: diskinfo v256 reports v0's 4MB; data written through v256 reads back through v0. Patched vinum.ko refuses the node (ENOENT, v0 intact). Root vinum-config privilege class.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5062 Β· 15 files
FileTypeDescriptionSize
df5062_volalias.sh β€” 2.9 KB view raw
alias2.sh β€” 596 B view raw
run.sh β€” 141 B view raw
run.log β€” 35.9 KB view raw
run.2.log β€” 1.4 KB view raw
build.log β€” 171 B view raw
fix.diff β€” 2.4 KB view raw
fix_build.log β€” 4.0 KB view raw
fix_run.log β€” 35.9 KB view raw
fix_run2.log β€” 968 B view raw
env.txt β€” 782 B view raw
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.0 KB view raw

DF-5062 β€” vinum volume minor aliasing (volno >= 256 wraps into VOL[0])

What

made_vol() (sys/dev/raid/vinum/vinumconfig.c:763) creates each /dev/vinum/vol/<name> node with VINUMDEV(vol->volno, 0, 0, VINUM_VOLUME_TYPE) and never checks that the volume number fits in the VINUM_VOL_WIDTH = 8 low bits of the minor (sys/dev/raid/vinum/vinumvar.h:83-86,113-116). Nothing else bounds the volume count: get_empty_volume() (vinumconfig.c:875-899) grows VOL[] indefinitely. Creating the 257th volume gives volume #256 the minor 0x100, which Volno() (sys/dev/raid/vinum/vinumutil.c:241) decodes as volno 0 β€” every open/read/write/ioctl/DIOCGPART on /dev/vinum/vol/v256 is served by VOL[0], a different volume. Deterministic cross-volume data corruption and wrong-object sizing from ordinary root vinum configuration.

Kernel minor() returns the raw si_uminor (sys/kern/kern_conf.c:70-75), so the wrap reaches vinum even though makeudev() (kern_conf.c:154-160) flags minor 0x100 as NOUDEV for the stat path β€” that is why ls -l /dev/vinum/vol/v256 displays the bogus rdev 255, 0xffff00ff while open()/read()/write() still execute against VOL[0].

How to reproduce (root on the DragonFly guest)

# push label.bin (from findings/poc/DF-4592) + df5062_volalias.sh
kldload vinum
sh /tmp/df5062_volalias.sh        # creates vn0 drive, v0 (4 MB sd), v1..v256
sh /tmp/alias2.sh                 # explicit data-aliasing check (od output)

Expected on a vulnerable kernel:

  1. /dev/vinum/vol/v256 exists with the bogus rdev 255, 0xffff00ff (v0/v255 show 32, 0x00000000 / 32, 0x000000ff).
  2. diskinfo /dev/vinum/vol/v256 reports v0's size (0x400000 = 4 MB), though v256 has zero plexes and size 0.
  3. Writing BBBB-v256-ALIAS through /dev/vinum/vol/v256 and reading back through /dev/vinum/vol/v0 returns BBBB β€” writes through v256 land in v0's storage.

All three reproduced (run.log, run.2.log).

Fix validation

fix.diff (against sys/, applied in the guest's /usr/src, vinum.ko rebuilt standalone, installed, rebooted):

  • kernel logs vinum: volume v256: volume number 256 does not fit in the 8-bit device minor; no device node created;
  • /dev/vinum/vol/v256 no longer exists (write β†’ ENOENT);
  • positive control intact: v0 still reads/writes its own data (fix_run.log, fix_run2.log).

Notes

  • Root-gated (vinum configuration requires the root-only superdevice, vinum.c:457-468) β€” same threat model as the rest of the vinum family.
  • The same wrap turns volume #512's node into minor 0x200 β†’ VOL[0] again, #768 β†’ 0x300, etc.; every multiple of 256 aliases onto VOL[0].
  • fix.diff also carries the DF-5063 hardening hunk for vinumutil.c (bounds-checked composite-minor decode); both were validated together.
VERDICT.md
↓ download raw

DF-5062 β€” VERDICT

Status: REPRODUCED (deterministic, twice, plus fix-validated). Impact: deterministic cross-volume data corruption + wrong-object sizing. Root-gated (vinum config path), hence Medium, not High.

Root cause (line-accurate)

  • sys/dev/raid/vinum/vinumvar.h:83-86 β€” VINUM_VOL_SHIFT=0, VINUM_VOL_WIDTH=8: a volume device minor holds the volume number in 8 bits.
  • sys/dev/raid/vinum/vinumvar.h:113-116 β€” VINUMDEV(v,p,s,t) = v<<0 | p<<16 | s<<20 | t<<28; nothing masks v.
  • sys/dev/raid/vinum/vinumconfig.c:759-774 β€” made_vol() does make_dev(&vinum_ops, VINUMDEV(vol->volno,0,0,VINUM_VOLUME_TYPE), ...) with no range check on vol->volno; get_empty_volume() (vinumconfig.c:875-899) expands VOL[] without limit.
  • sys/dev/raid/vinum/vinumutil.c:238-242 β€” Volno(dev) = (minor(dev) & MASK(8)) >> 0 β†’ minor 0x100 decodes as volno 0.
  • sys/kern/kern_conf.c:70-75 β€” kernel minor(cdev_t) returns the raw si_uminor (256), so vinum really is entered with 0x100; kern_conf.c:154-160 makeudev() only rejects it for the stat path (ls -l shows 255, 0xffff00ff β€” the NOUDEV pattern), which is cosmetic.

Reproduction evidence

run.log (stock module, fresh vm.sh reset with-src boot): * 257 volumes created (vinum l header: 257 volumes:). * ls -l /dev/vinum/vol/v256 β†’ crw- 255, 0xffff00ff (v0: 32, 0x00000000). * diskinfo /dev/vinum/vol/v256 β†’ size=0x000000400000 4.00 MB β€” exactly v0's size, while v256 has Plexes: 0 Size: 0 B. * run.2.log (explicit od dump): write AAAA-v0-mark via v0 β†’ read via v0 = AAAA; write BBBB-v256-ALIAS via v256 β†’ read via v0 = BBBB. Writes through v256 land in v0's storage.

Impact ceiling

Any vinum deployment with β‰₯257 configured volumes (volume numbers are handed out sequentially and never reused until removal): all I/O to the aliased node silently reads/writes a different volume β€” filesystem corruption, swap-on-wrong-volume (DIOCGPART serves v0's geometry), and the bogus st_rdev shown by stat. Attack path is the ordinary root vinum configuration interface (vinum create -f), the same privilege class as every other vinum finding in this audit (superdev open is SYSCAP_RESTRICTEDROOT, vinum.c:457-468).

Fix validation (done in this run)

fix.diff applied to the guest's /usr/src, vinum.ko rebuilt standalone (make rc=0, fix_build.log), installed to /boot/kernel, rebooted, PoC re-run (fix_run.log, fix_run2.log): * kernel logs vinum: volume v256: volume number 256 does not fit in the 8-bit device minor; no device node created; * /dev/vinum/vol/v256 absent; write attempt β†’ ENOENT (aliasing gone); * positive control: v0 data I/O intact (PATCHED-OK/AAAA readbacks).

Not pursued

The minor-wrap also corrupts plex/sd sub-minor fields for volno β‰₯ 65536 (volno bits spill into bits 16+), but 65k+ volumes is not a realistic configuration; the 8-bit wrap is the practical case.

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

vinum.ko rebuilt from patched /usr/src in the guest (make rc=0), installed, rebooted, exact PoC re-run: kernel logs 'vinum: volume v256: volume number 256 does not fit in the 8-bit device minor; no device node created', /dev/vinum/vol/v256 is absent (write -> ENOENT, diskinfo -> No such file), and the positive control /dev/vinum/vol/v0 still reads/writes its own data. Aliasing gone; fix_build.log/fix_run.log/fix_run2.log.

findings/poc/DF-5062/fix_run.log (patched log line + missing node + v0 4.00 MB control); fix_run2.log (AAAA survives, BBBB write via v256 -> ENOENT); fix_build.log (clean rebuild); fix.diff in the same pack
↓ fix.diffper-fix-DF-5062

Confirmed kernel references

Detail

Exploit chain

root: vinum create -f (257 volumes) -> /dev/vinum/vol/v256 (minor 0x100) aliases VOL[0] -> all I/O through the v256 node silently reads/writes volume v0's storage: filesystem/swap corruption of the wrong volume (data-integrity attack, not kernel-memory corruption)

Evidence (decisive lines)

run.log: '257 volumes:', ls -l v256 rdev 255,0xffff00ff, diskinfo v256 'size=0x400000 4.00 MB'; run.2.log: od dump showing BBBB-v256-ALIAS written via v256 read back via v0; fix_run.log/fix_run2.log: patched module logs 'volume number 256 does not fit in the 8-bit device minor', v256 ENOENT, v0 positive control intact

PoC changes

wrote df5062_volalias.sh from scratch (no seed): vn0 vnode disk + vinum label (label.bin from the DF-4592 pack) since the guest has no mdconfig(8); first attempt used mdconfig (absent) so the drive failed and v0 stayed down (all EIO) -- switched to vnconfig(8), added 'vinum start v0' and conv=sync dd markers with od readback

Verified recommended fix

made_vol(): refuse to make_dev a volume whose number does not fit in VINUM_VOL_WIDTH bits (log + return); see fix.diff

Verdict

made_vol() (sys/dev/raid/vinum/vinumconfig.c:763) creates /dev/vinum/vol/ with VINUMDEV(volno,0,0,VINUM_VOLUME_TYPE) and no range check, while get_empty_volume() grows VOL[] without limit; VINUM_VOL_WIDTH is 8 bits, so volume #256 gets minor 0x100 which Volno() (vinumutil.c:241) decodes as volno 0. Kernel minor() returns the raw si_uminor (kern_conf.c:70-75), so open/read/write/ioctl on /dev/vinum/vol/v256 are all served by VOL[0] -- a different volume. Reproduced deterministically on the stock-module guest: diskinfo(/dev/vinum/vol/v256) returned v0's exact 4.00 MB size for a zero-plex volume, and a BBBB marker written through v256 read back through /dev/vinum/vol/v0 (run.log, run.2.log). Deterministic cross-volume data corruption + wrong-object DIOCGPART sizing from ordinary root vinum configuration; every 256th volume aliases onto VOL[0]. Root-gated like the rest of the vinum family. Fix validated on a rebuilt vinum.ko: v256 gets no device node (log line + ENOENT), v0 I/O intact.