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

vinumsize() serves d_psize for every device type and indexes VOL[] with the subdisk/plex number β€” wrong-object swap sizing (deterministic) and heap OOB read for sdno/plexno >= volumes_allocated

Summary

No DEVTYPE dispatch, no bounds check: raw sd/plex minors encode the sd/plex number in the bits Volno() extracts - in-bounds returns the volume's size for a subdisk device, N>=volumes_allocated is a heap OOB read. Sole caller is swapon (SYSCAP_RESTRICTEDROOT). REPRODUCED on guest: swapon /dev/vinum/sd/s0 installed 6MB of swap backing on a 1MB subdisk (silent disk corruption under swap pressure); s4/s5 OOB. Fix (type dispatch + bound) validated on rebuilt module: sd swapons ENXIO, volume path intact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4758 Β· 13 files
FileTypeDescriptionSize
df4758_swapon.c β€” 2.0 KB view raw
run.sh β€” 297 B view raw
run2.sh β€” 673 B view raw
df4758_run.log β€” 3.7 KB view raw
df4758_run2.log β€” 2.6 KB view raw
build.log β€” 152 B view raw
fix.diff β€” 2.5 KB view raw
fix_run.log β€” 2.0 KB view raw
env.txt β€” 665 B view raw
README.md β€” 4.0 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 1.1 KB view raw

DF-4758 β€” vinum vinumsize() answers d_psize for the wrong object type and reads VOL[] out of bounds (raw sd/plex minors index the volume array with the subdisk/plex number)

  • File: sys/dev/raid/vinum/vinum.c:558-572 (vinumsize)
  • Severity: Medium (root-gated; heap OOB read + provably wrong swap sizing β†’ swap writes beyond the device extent)
  • Bucket: memcorrupt / base:raid

What

vinumsize() is the d_psize handler of every vinum device (one dev_ops for all types). It does:

vol = &VOL[Volno(dev)];          /* vinum.c:564 */
if (vol->state == volume_up) {
    ap->a_result = (int64_t)vol->size;

with no DEVTYPE() dispatch and no bounds check. Volno(dev) is a raw bit extraction (minor & 0xff, vinumutil.c:239-243). Raw subdisk and raw plex minors are built by VINUM_SD(s) / VINUM_PLEX(p) (vinumvar.h:124-132), which encode the subdisk/plex number in the low 8 bits β€” exactly the bits Volno() returns. So:

  • d_psize on /dev/vinum/sd/sN indexes VOL[N] β€” the volume array β€” with the subdisk number;
  • d_psize on /dev/vinum/plex/pN indexes VOL[N] with the plex number;
  • whenever that number >= vinum_conf.volumes_allocated (4 initially, INITIAL_VOLUMES, vinumvar.h:194) it is a kernel heap OOB read of sizeof(struct volume) * N bytes past the array (state at +64, size at +104 of the phantom entry);
  • even for in-bounds numbers it returns the volume's size (and only if that volume is up) instead of the subdisk's/plex's own size.

The only kernel caller of d_psize is dev_dpsize() from the swapon path (sys/vm/vm_swap.c:308), gated by caps_priv_check_self (SYSCAP_RESTRICTEDROOT) (vm_swap.c:202) β€” root-only.

Reproduced (root on the DragonFly guest, stock kernel)

Config: 1 volume v0 (up, 6144 kB), 1 concat plex, 6 subdisks s0..s5 (1024 kB each, all up); volumes_allocated == 4.

$ swapon /dev/vinum/sd/s0          # a 1 MB subdisk (2048 512-blocks)
OK
$ swapinfo
Device          512-blocks     Used    Avail Capacity  Type
/dev/vinum/sd/s0      12288        0    12288     0%    Interleaved

12288 blocks = 6 MB = exactly volume v0's size β€” vinumsize() sized the subdisk device from the wrong object. The kernel now believes a 1 MB subdisk provides 6 MB of swap backing; under swap pressure it writes swap pages at offsets 2048..12287 blocks, past the end of the subdisk, onto whatever occupies those offsets on the drive (other subdisks / vinum config / other data) β€” silent data corruption plus unrecoverable swap-in of garbage pages.

s4/s5 (sdno β‰₯ volumes_allocated) executed the OOB indexing VOL[4]/VOL[5] past the 4-entry array; on this boot the heap bytes at those offsets did not equal volume_up, so swapon returned ENXIO β€” the result of the OOB read is heap-layout-dependent (a matching state word installs a garbage size, e.g. a huge value β†’ vm_swap.c:344 rejects with "exceeded maximum of %ld blocks per swap unit" on console; a mid-range garbage value installs quietly).

Files

  • df4758_swapon.c β€” trigger (swapon(2) on device paths, prints exact errno + swapinfo)
  • run.sh β€” full fresh-boot repro (label + drive + volume/plex/6 sds + probes)
  • run2.sh β€” probe-only (config already exists)
  • df4758_run.log, df4758_run2.log β€” full untrimmed baseline runs
  • fix.diff β€” combined vinum.c fix (vinumsize hunk + the DF-4760 hunks)
  • fix_run.log β€” patched-module validation (bad behavior GONE, volume path still works)
  • build.log β€” trigger build

Build / run (guest, root)

kldload vinum
dd if=label.bin of=/dev/md0 bs=512 seek=1 conv=notrunc   # from findings/poc/DF-4592
cc -O -o /tmp/df4758_swapon /tmp/df4758_swapon.c
sh run.sh          # or run2.sh after the config exists

Expected

  • Vulnerable: swapon /dev/vinum/sd/s0 β†’ OK, swapinfo shows the volume's size (12288) on a 1 MB subdisk; sd β‰₯ 4 probes OOB-read VOL[] (ENXIO / garbage-size / panic depending on heap).
  • Patched: every sd/plex swapon β†’ ENXIO; /dev/vinum/vol/v0 β†’ OK with the correct 12288 blocks.
VERDICT.md
↓ download raw

DF-4758 β€” VERDICT

REPRODUCED β€” status reproduced, impact dos (demonstrated wrong-object swap sizing with OOB indexing; ceiling is silent disk/memory corruption), confidence certain.

What was run

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (stock snapshot, X86_64_GENERIC, 2026-09-23). Module: stock /boot/kernel/vinum.ko. All as root (the swapon path is SYSCAP_RESTRICTEDROOT-gated, sys/vm/vm_swap.c:202).

Config (run.sh): vinum drive d1 on /dev/md0s0a, volume v0 (up, 6144 kB), concat plex p0, subdisks s0..s5 (1024 kB each, all up). volumes_allocated == 4 (INITIAL_VOLUMES).

The decisive observation

swapon /dev/vinum/sd/s0 β€” a 1 MB subdisk β€” succeeded and installed 12288 512-blocks (6 MB) of swap, exactly the size of volume v0:

== swapon(/dev/vinum/sd/s0): OK
Device          512-blocks     Used    Avail Capacity  Type
/dev/vinum/sd/s0      12288        0    12288     0%    Interleaved

Root cause chain, line by line: 1. swapctl(2) β†’ swap_on (vm_swap.c:296-308): VOP_OPEN then dev_dpsize(dev) because nblks == 0. 2. dev_dpsize β†’ vinum_ops.d_psize = vinumsize (vinum.c:58-70 β€” one ops table for every vinum device type). 3. vinumsize (vinum.c:564): vol = &VOL[Volno(dev)] β€” no DEVTYPE check, no bounds check. 4. Volno(dev) = minor & 0xff (vinumutil.c:239-243); raw-sd minors are VINUM_SD(sdno) = (RAWSD_TYPE<<28) | (sdno & 0xff) | ... (vinumvar.h:129-132) β†’ Volno(/dev/vinum/sd/s0) == 0. 5. VOL[0] is volume v0 (up) β†’ returns v0's size for the subdisk.

s0 is the in-bounds proof (wrong object, deterministic). s4/s5 (sdno >= volumes_allocated) prove the OOB: VOL[4]/VOL[5] read 4/5 * sizeof(struct volume) bytes past the 4-entry kmalloc'd array (state at +64, size at +104). On this boot the OOB state words were not volume_up (3), so swapon returned ENXIO β€” the observable result of an OOB read is heap-layout-dependent; a matching word installs a garbage size (huge β†’ vm_swap.c:344 "exceeded maximum" ENXIO; mid-range β†’ quietly wrong swap size, as demonstrated in-bounds by s0).

Impact ceiling

The kernel trusts vinumsize() for swap sizing: a 6Γ— oversized swap device on a 1 MB subdisk means swap-out at offsets past the subdisk extent β†’ writes onto unrelated drive contents (silent corruption of adjacent vinum objects / config / data) and swap-in of garbage pages (memory corruption). All gated on root (swapon + the vinum config), hence severity Medium, not a privilege boundary crossing; the heap OOB read itself can also fault or read attacker-influenceable heap (panic / limited info side channel via installed swap size).

Fix validation (performed in the same run)

fix.diff (this pack) adds DEVTYPE == VINUM_VOLUME_TYPE dispatch and an index >= volumes_allocated bounds check. Rebuilt vinum.ko standalone from patched /usr/src (make in sys/dev/raid/vinum, BUILD_RC=0), installed, reloaded:

  • swapon /dev/vinum/sd/s0|s1|s4|s5 β†’ ENXIO (baseline: s0 OK with the bogus 12288-block size). swapinfo clean. BAD BEHAVIOR GONE.
  • Positive control swapon /dev/vinum/vol/v0 β†’ OK, 12288 blocks (the volume's true size) β†’ legit volume psize still works.

fix_baseline_reproduced=1, fix_patched_reproduced=0 (bug does not reproduce on the patched module), fix_status=fixed.

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 on the guest (make rc=0), installed, loaded: swapon on every sd device now ENXIO with swapinfo clean (baseline: s0 OK with bogus 12288-block install), and the positive control /dev/vinum/vol/v0 still installs the correct 12288-block size. Bad behavior gone.

findings/poc/DF-4758/fix_run.log (patched run + baseline comparison + positive control); fix.diff in the same pack
↓ fix.diffper-fix-DF-4758

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4758/df4758_run2.log: 'swapon(/dev/vinum/sd/s0): OK' followed by swapinfo '/dev/vinum/sd/s0 12288' (volume size on a subdisk); fix_run.log: patched module rejects all sd swapons (ENXIO) while /dev/vinum/vol/v0 still installs the correct 12288 blocks

PoC changes

seed had none; wrote df4758_swapon.c (swapon(2) per device path with exact errno + swapinfo) and run.sh/run2.sh; first build failed on a nonexistent sys/swapon.h include (removed; swapon lives in unistd.h), second build clean

Verified recommended fix

vinumsize(): dispatch on DEVTYPE(dev) == VINUM_VOLUME_TYPE and bounds-check Volno(dev) against vinum_conf.volumes_allocated before touching VOL[] (see fix.diff)

Verdict

vinumsize() (sys/dev/raid/vinum/vinum.c:564) indexes the volume array with Volno(dev) with no DEVTYPE dispatch and no bounds check; raw sd/plex minors encode the subdisk/plex number in the bits Volno() extracts, so d_psize on /dev/vinum/sd/sN reads VOL[N]. Reproduced on the stock-kernel guest: swapon /dev/vinum/sd/s0 (a 1 MB subdisk) installed 12288 512-blocks of swap -- exactly volume v0's 6 MB size -- proving the wrong-object lookup deterministically; s4/s5 executed OOB reads past the 4-entry VOL array (heap-dependent ENXIO on this boot). Impact ceiling: swap sized from the wrong object -> swap I/O beyond the subdisk extent (silent disk/data corruption, garbage swap-in), plus heap OOB read. Root-gated (swapon SYSCAP_RESTRICTEDROOT).