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)
PoC verification
Evidence pack
findings/poc/DF-4758 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_psizeon/dev/vinum/sd/sNindexesVOL[N]β the volume array β with the subdisk number;d_psizeon/dev/vinum/plex/pNindexesVOL[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 ofsizeof(struct volume) * Nbytes past the array (stateat +64,sizeat +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 runsfix.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/plexswapon β ENXIO;/dev/vinum/vol/v0β OK with the correct 12288 blocks.
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).swapinfoclean. 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
fixedvinum.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
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).
No comments yet.