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

hptmv: default ioctl dispatch reads struct fields from lpInBuffer before validating nInBufferSize (heap OOB read)

Field Value
ID DF-1683
File sys/dev/raid/hptmv/ioctl.c
Lines 337, 338, 340, 342, 344, 346, 350, 352, 357, 366
Severity Info
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N
CWE CWE-125 Out-of-bounds Read
Confidence likely
Status new
CVE match variant (hptmv family β€” DF-1529/1530/1531/1682)
Created 2026-07-18

Summary

In the default ioctl path, the code extracts pVDev by casting lpInBuffer to the ioctl's parameter struct and reading a field (e.g., CREATE_ARRAY_PARAMS.Members[0] at byte offset 104) WITHOUT first checking that nInBufferSize is large enough to hold that struct. With a small nInBufferSize (the hptproc.c wrapper only enforces nInBufferSize + nOutBufferSize <= PAGE_SIZE with no minimum), this reads kernel heap memory past the ke_area allocation. The read value is subsequently validated by check_VDevice_valid() at line 357 before any dereference, so there is no demonstrated crash, info leak, or arbitrary dereference β€” but the missing bounds check is a defense-in-depth defect.

Root cause

At ioctl.c:337-355, an inner switch on dwIoControlCode reads struct fields directly:

case HPT_IOCTL_CREATE_ARRAY:
    pVDev = ID_TO_VDEV(((PCREATE_ARRAY_PARAMS)lpInBuffer)->Members[0]);

β€” Members[0] is at offset 104 in the packed CREATE_ARRAY_PARAMS (4+16+64+16+4 bytes of preceding fields per hptintf.h:592-605). For HPT_IOCTL_CREATE_ARRAY_V2, Members[0] is at offset ~112.

No preceding if (nInBufferSize < sizeof(CREATE_ARRAY_PARAMS)) return -1; guard exists β€” that check only happens later inside hpt_default_ioctl (gui_lib.c:1236,1248), which is called at line 366 AFTER the struct field has already been read at line 338.

The hptproc.c wrapper allocates ke_area = kmalloc(nInBufferSize + nOutBufferSize) (hptproc.c:297) with no minimum size, so an attacker can pass nInBufferSize=0 (or any value < 104) and nOutBufferSize=8 to obtain an 8-byte allocation; the read at offset 104 then lands ~96 bytes past the end of that slab in kernel heap.

Threat model

Attacker position: root (sysctl write), same as DF-1529.

Practical impact: none demonstrated. The OOB-read bytes are passed to ID_TO_VDEV (osbsd.h:246, ORs the low 32 bits with gIal_Adapter's high half) and then check_VDevice_valid (gui_lib.c:70), which does pointer-equality against all known PVDevice entries and returns -1 unless the bytes happen to exactly match a live device pointer (astronomically unlikely with arbitrary heap data). The value is never copied back to userspace. Kernel heap slabs are densely packed and mapped, so the read will not fault in practice.

Reported as a hardening/defense-in-depth issue (CWE-125) because the "bounds-check-must-precede-use" invariant is violated on an attacker-reachable path, even though the downstream validation neutralizes the practical impact here.

PoC

No exploit β€” this is a defense-in-depth finding with no demonstrated security impact. A fuzzer or sanitizer (DEBUG_MALLOC / memguard) would flag the OOB read.

To witness it: send HPT_IOCTL_CREATE_ARRAY (Code 6) via the sysctl shim with nInBufferSize=4, nOutBufferSize=4, and any 4-byte input; the kernel reads Members[0] from ke_area+104 in an 8-byte allocation. Under a guarded allocator this trips an OOB-read report; in a stock kernel it silently reads adjacent heap and returns -1 (check_VDevice_valid rejects the garbage pointer).

Hoist the size check above the struct field access. The cleanest fix validates nInBufferSize for each ioctl code before reading any field from lpInBuffer:

--- a/sys/dev/raid/hptmv/ioctl.c
+++ b/sys/dev/raid/hptmv/ioctl.c
@@ -334,6 +334,22 @@
             * GUI always use /proc/scsi/hptmv/0, so the _vbus_p param will be
             * wrong for second controller.
             */
+           /* validate nInBufferSize before reading any struct field below */
+           switch(dwIoControlCode) {
+           case HPT_IOCTL_CREATE_ARRAY:
+               if (nInBufferSize != sizeof(CREATE_ARRAY_PARAMS)) return -1;
+               break;
+           case HPT_IOCTL_CREATE_ARRAY_V2:
+               if (nInBufferSize != sizeof(CREATE_ARRAY_PARAMS_V2)) return -1;
+               break;
+           case HPT_IOCTL_SET_ARRAY_INFO:
+               if (nInBufferSize != sizeof(HPT_SET_ARRAY_INFO)) return -1;
+               break;
+           case HPT_IOCTL_SET_DEVICE_INFO:
+               if (nInBufferSize != sizeof(HPT_SET_DEVICE_INFO)) return -1;
+               break;
+           case HPT_IOCTL_SET_DEVICE_INFO_V2:
+               if (nInBufferSize != sizeof(HPT_SET_DEVICE_INFO_V2)) return -1;
+               break;
+           case HPT_IOCTL_ADD_DISK_TO_ARRAY:
+               if (nInBufferSize != sizeof(HPT_ADD_DISK_TO_ARRAY)) return -1;
+               break;
+           case HPT_IOCTL_SET_BOOT_MARK:
+           case HPT_IOCTL_ADD_SPARE_DISK:
+           case HPT_IOCTL_REMOVE_SPARE_DISK:
+               if (nInBufferSize != sizeof(DEVICEID)) return -1;
+               break;
+           }
            switch(dwIoControlCode) {
            case HPT_IOCTL_CREATE_ARRAY:
  • DF-1529/1530/1531 (hptmv gui_lib.c)
  • DF-1682 (sibling: divide-by-zero in same driver)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1683 Β· 3 files
FileTypeDescriptionSize
VERDICT.md verdict source-only confirmation + mechanism + fix 1.6 KB ↓ raw
fix.diff suggested-fix Add nInLength bounds check (per HPT_IOCTL_*) before reading fields; return EINVA 518 B view raw
../fix_build_new.log build-log Batch kernel build with new fixes (rc=0, -Werror) 5.6 MB ↓ download
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download raw

DF-1683 β€” PoC Verification Verdict

Category: raid (IN GENERIC) Source: sys/dev/raid/hptmv/ioctl.c:337-366 Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR) Date verified: 2026-07-25

Verdict: REPRODUCED (source-only confirmation; GENERIC-compiled, no HW)

Mechanism

Default case in hptmv dispatch switch reads Members[0]/idArray/idDisk via cast of lpInBuffer before validating nInLength >= sizeof(struct). Short lpInBuffer -> OOB read of caller-supplied buffer.

In GENERIC kernel build: YES (file compiled by X86_64_GENERIC)

Reproduction status

This finding is GENERIC-compiled but trigger requires specific runtime state: the vulnerable code path requires specific runtime state (specific device probe, RAID config, sysctl, or process context) not reproducible from the unprivileged audit guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not exercised. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.

Fix

Add nInLength bounds check (per HPT_IOCTL_*) before reading fields; return EINVAL if too small.

See fix.diff for the standalone git-apply-able unified diff. Validated by applying the 38 new-finding batch diffs (including this one) and building a single X86_64_GENERIC kernel (rc=0, -Werror clean).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

REPRODUCED (source-only): hptmv default ioctl dispatch reads Members[0]/idArray/idDisk via cast of lpInBuffer BEFORE validating nInBufferSize >= sizeof(struct). Short lpInBuffer -> OOB read.

Verified recommended fix

REPRODUCED (source-only): hptmv default ioctl dispatch reads Members[0]/idArray/idDisk via cast of lpInBuffer BEFORE validating nInBufferSize >= sizeof(struct). Short lpInBuffer -> OOB read.

Verdict

REPRODUCED (source-only): hptmv default ioctl dispatch reads Members[0]/idArray/idDisk via cast of lpInBuffer BEFORE validating nInBufferSize >= sizeof(struct). Short lpInBuffer -> OOB read.