Plexno()/Sdno() decode composite plex/sd minors with no bounds checks and propagate the -1 empty-slot sentinel into PLEX[-1].sdnos[] (latent heap OOB read; unreachable on current DragonFly)
Summary
Composite-minor decode walks VOL[]/PLEX[] unvalidated; -1 empty-slot sentinel survives the >=-only caller checks into PLEX[-1]. Latent: no kernel make_dev creates composite minors, and hand-mknod'ed char nodes cannot reach vinumopen (they panic upstream in the generic devfs/ufs layer - separate bug routed). Bounds-check fix compiled+regression-validated on rebuilt vinum.ko.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5063 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5063_mknod.c | β | 1.2 KB | view raw | |
| df5063_sdcomp.sh | β | 3.2 KB | view raw | |
| df5063_setup.sh | β | 815 B | view raw | |
| df5063_probe.sh | β | 2.1 KB | view raw | |
| run.sh | β | 211 B | view raw | |
| run.log | β | 1.7 KB | view raw | |
| run.2.log | β | 1.3 KB | view raw | |
| run.3.log | β | 181 B | view raw | |
| panic.txt | β | 4.7 KB | view raw | |
| fix.diff | β | 2.4 KB | view raw | |
| fix_run.log | β | 2.0 KB | view raw | |
| fix_run2.log | β | 521 B | view raw | |
| env.txt | β | 782 B | view raw | |
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-5063 β vinum Plexno()/Sdno() composite-minor decode walks tables unvalidated (latent OOB)
What
Plexno() and Sdno() (sys/dev/raid/vinum/vinumutil.c:250-297) decode the
composite VINUM_PLEX_TYPE / VINUM_SD_TYPE minor layouts with no bounds
checks, and propagate the -1 "empty slot" sentinel that
get_empty_volume() stores in vol->plex[] (vinumconfig.c:896-897):
case VINUM_PLEX_TYPE: /* vinumutil.c:261 */
case VINUM_SD_TYPE:
return VOL[Volno(dev)].plex[(minor(dev) >> 16) & MASK(3)]; /* may be -1 */
...
case VINUM_SD_TYPE: /* vinumutil.c:289 */
return PLEX[Plexno(dev)].sdnos[(minor(dev) >> 20) & MASK(8)]; /* PLEX[-1]! */
A VINUM_SD_TYPE minor whose plex ordinal addresses an empty slot makes
Plexno() return β1, and Sdno() then reads PLEX[-1].sdnos[ordinal] β
a heap out-of-bounds read up to ~1 KB before the plex[] array whose value
is then used as an index into SD[]. Callers only check >=
(vinum.c:430-440), which β1 survives as a signed value.
Reachability β why this is only Info
The kernel never creates composite-minor device nodes: the only
make_dev sites are VINUM_SD(sdno) (rawsd), VINUM_PLEX(plexno)
(rawplex) and volume nodes (vinumconfig.c:740,763,780). The sole way to
feed a composite minor to vinum is a hand-made node (mknod).
On this DragonFly (6.5-DEVELOPMENT guest) that route dies before vinum.
Reading a hand-mknod'ed character node β even with a valid vinum rawplex
minor (0x50000000) β panics the kernel in the generic device-resolution
layer: Fatal trap 12 ... Stopped at dev_dflags: movq 0xa8(%rdi),%rax
with rdi = 0 (NULL cdev), current process dd (panic.txt; reproduced
twice). UFS-mknod'ed VCHR vnodes never get a resolved v_rdev
(addaliasu() only stores v_umajor/v_uminor, sys/kern/vfs_subr.c:1246-1252)
and some path dereferences it unchecked. So Plexno()/Sdno()'s composite
branches are dead code today β reported as hardening (defense-in-depth, and
the identical PLEX[-1] sink is already exploitable through the config
path per DF-1399).
What was run (root on the guest)
df5063_setup.shβ vn0 drive, v0 (plex p0, sds s0/s1), up.df5063_sdcomp.shβ original mknod(1) sweep: the guest has no mknod binary (run.log).df5063_mknod.cβ mknod(2)+open(2) probe tool (built clean).- Step-by-step probe: reading
/tmp/px0(mknod'ed valid rawplex minor) wedged the guest βFatal trap 12indev_dflags(panic.txt). Composite minors never reached vinum.
Fix (validated as part of DF-5062's rebuild)
fix.diff (same file as DF-5062's pack) adds explicit bounds checks to
both composite branches of Plexno()/Sdno() (volno against
volumes_allocated, plexno against plexes_allocated, -1 never used
to index). Patched-module regression (fix_run.log, fix_run2.log):
raw sd / rawplex / volume read-write all intact.
Cross-file observation for the orchestrator
The mknod'ed-node read NULL-deref (dev_dflags(NULL)) is a separate,
non-vinum bug in the UFSβspec/devfs chain, root-gated local DoS,
reproducible on demand (see panic.txt + run.log/run.2.log). Suggest a
spawn on sys/vfs/devfs/devfs_vnops.c / sys/kern/vfs_subr.c.
DF-5063 β VERDICT
Status: NOT REPRODUCED (unreachable on this system; Info hardening). The code defect is certain by inspection; the composite-minor paths that reach it are dead code on DragonFly, and the only way to synthesize such a minor (a hand-made device node) panics the kernel upstream of vinum.
The defect (certain, static)
sys/dev/raid/vinum/vinumutil.c:261βPlexno()VINUM_PLEX_TYPE/SD_TYPE case:VOL[Volno(dev)].plex[ordinal]with no check ofVolno(dev)againstvolumes_allocatedand no handling of the-1stored in emptyvol->plex[]slots (vinumconfig.c:896-897).sys/dev/raid/vinum/vinumutil.c:289βSdno()VINUM_SD_TYPE case:PLEX[Plexno(dev)].sdnos[ordinal]; whenPlexno()returns β1 this is a heap OOB read atPLEX_base β sizeof(struct plex) + offsetof(sdnos) + 4*ordinal(β β112 β¦ +908 bytes around the allocation), whose garbage value is then used to indexSD[].- Callers check only
>=: vinum.c:430-431 (Plexno(dev) >= plexes_allocatedis false for β1, signed) and vinum.c:438 (the OOB-read value must only be< subdisks_allocatedand pass the SD state check). - Same terminal sink as DF-1399 (config kw_prefer path), different entry.
Why unreachable at runtime here
- No kernel
make_devcreates composite minors β onlyVINUM_SD(sdno)/VINUM_PLEX(plexno)(raw) and volume nodes exist (vinumconfig.c:740,763,780; verified on the guest:/dev/vinum/sd/s0= 0x60000000,/dev/vinum/plex/p0= 0x50000000). - A hand-made node cannot reach vinumopen: reading an mknod'ed char node
(VALID rawplex minor 0x50000000) panics in
dev_dflags(NULL)before any vinum code runs β twice reproduced (panic.txt; step-1 probe). Root cause of that panic is in the generic UFSβspec/devfs resolution (no resolver populatesv_rdevfor mknod'ed VCHR vnodes on DF;addaliasuonly stores v_umajor/v_uminor β sys/kern/vfs_subr.c:1246). β separate cross-file bug, root-gated DoS, not vinum's.
Runs
- run.log β full sweep with mknod(1): guest has no mknod binary; setup OK.
- run.2.log β mknod(2) tool first used; argc bug in the tool (fixed).
- run.3.log β corrected probe: hung β panic (dev_dflags NULL, proc dd).
- Step-by-step after reset: FIRST hand-made node read (valid minor!) β panic again β attribution: generic layer, not composite minors.
- fix_run.log / fix_run2.log β patched-module regression: raw sd/plex/vol I/O all correct after the Plexno/Sdno bounds checks (fix.diff shared with DF-5062, rebuilt vinum.ko).
Disposition
File as Info (hardening): the unvalidated walks should be bounded so the latent PLEX[-1] cannot be resurrected by any future composite-node creation. No runtime impact demonstrated (and demonstrably blocked upstream today). Recommended fix included and compile+regression validated.
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 cannot be exercised at runtime (the composite path is unreachable before and after -- no behavior change observable by design). Validated by clean rebuild of vinum.ko with the extra checks (fix_build.log via DF-5062 pack) plus a regression run: raw sd/plex/volume reads and volume write+readback all correct on the patched module (fix_run.log, fix_run2.log).
findings/poc/DF-5063/fix_run2.log (SD-S0-DATA via /dev/vinum/sd/s0, /dev/vinum/plex/p0 and /dev/vinum/vol/v0; PATCHED-OK volume write/readback); fix.diff (shared with DF-5062)
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (Fatal trap 12 dev_dflags NULL after reading an mknod'ed node with a VALID minor -- composite minors die the same way before vinum); run.log (no mknod(1) on guest; device-node minors show raw-only: sd 0x60000000, plex 0x50000000); fix_run2.log (patched module regression: raw sd/plex/vol I/O intact)
PoC changes
wrote df5063_mknod.c (guest lacks mknod(1)); first version had an argc==5 bug for the 3-arg create form (fixed to argc==4); composite sweep never reached vinum because reading ANY hand-made char node panics the generic device layer first
Verified recommended fix
Plexno()/Sdno(): bounds-check volno/plexno against volumes_allocated/plexes_allocated and return -1 instead of indexing VOL[]/PLEX[] with -1 (see fix.diff)
Verdict
Plexno()/Sdno() (sys/dev/raid/vinum/vinumutil.c:261,289) decode composite VINUM_PLEX_TYPE/VINUM_SD_TYPE minors with no bounds checks and propagate the -1 stored in empty vol->plex[] slots: Sdno() computes PLEX[-1].sdnos[ordinal], a heap OOB read whose value then indexes SD[]; callers only check >= so -1 survives (vinum.c:430-440). The defect is certain by inspection, but the paths are UNREACHABLE on DragonFly: no make_dev site creates composite minors (only raw VINUM_SD/VINUM_PLEX and volume nodes, vinumconfig.c:740/763/780), and hand-mknod'ed char nodes never reach vinumopen -- reading one, even with a VALID vinum rawplex minor (0x50000000), panics earlier in the generic UFS->spec/devfs layer (Fatal trap 12, dev_dflags(NULL), rdi=0, fault va 0xa8, proc dd; reproduced twice, panic.txt) because nothing resolves v_rdev for mknod'ed VCHR vnodes. Downgraded to Info hardening; the identical PLEX[-1] sink remains reachable via the config path (DF-1399). Fix (bounds checks in Plexno/Sdno, shared fix.diff) applied and regression-validated on a rebuilt vinum.ko.
No comments yet.