32-bit integer overflow in hptmv HPT_IOCTL_PARAM size check enables kernel heap overflow
| Field | Value |
|---|---|
| ID | DF-1851 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-190 Integer Overflow or Wraparound; CWE-787 Out-of-bounds Write |
| File | sys/dev/raid/hptmv/hptproc.c |
| Lines | 292, 297, 304 |
| Area | dev/raid (HighPoint hptmv sysctl) |
| Confidence | certain |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | FreeBSD-SA-09:11.hptmv (same pattern) |
| CVE match | variant |
Summary
The size check piop->nInBufferSize+piop->nOutBufferSize > PAGE_SIZE is
evaluated in 32-bit unsigned arithmetic because both fields are DWORD
(= unsigned int, hptintf.h:748,750). An attacker can choose sizes whose true
sum exceeds 2^32 but whose 32-bit-wrapped sum is <= 4096, bypassing the check.
The subsequent kmalloc allocates the wrapped (small) size, but copyin uses
the original (huge) sizes, producing an unbounded kernel heap overflow.
Root cause
hptproc.c:292:
if (piop->nInBufferSize+piop->nOutBufferSize > PAGE_SIZE) {
KdPrintE(("User buffer too large\n"));
return -EINVAL;
}
Both nInBufferSize and nOutBufferSize are DWORD (hptintf.h:748,750 =
unsigned int). C performs the addition in unsigned int width, so e.g.
nInBufferSize=0x00000FFF (4095) + nOutBufferSize=0xFFFFF001 wraps modulo 2^32
to 0x00000001, which is not > PAGE_SIZE (4096).
hptproc.c:297 then calls:
ke_area = kmalloc(piop->nInBufferSize+piop->nOutBufferSize, M_DEVBUF, M_NOWAIT);
with the same wrapped value 1, returning a minimum-size slab object.
hptproc.c:304:
copyin((void*)(ULONG_PTR)piop->lpInBuffer, ke_area, piop->nInBufferSize);
is then invoked with the unwrapped nInBufferSize=4095, copying 4095 user
bytes into the ~16-byte slab allocation — a 4079-byte kernel heap overflow.
hptproc.c:310 Kernel_DeviceIoControl(... ke_area + piop->nInBufferSize,
piop->nOutBufferSize ...) passes a destination pointer far past the allocation
and the unwrapped 0xFFFFF001 length, giving every ioctl handler in ioctl.c
an OOB write window into adjacent kernel heap.
hptproc.c:315 copyout(ke_area + piop->nInBufferSize, lpOutBuffer,
piop->nOutBufferSize) reads OOB from kernel heap into userspace (info leak) if
Kernel_DeviceIoControl returns 0.
This bug pattern matches FreeBSD-SA-09:11.hptmv; the DragonFlyBSD copy still ships the buggy unbounded addition.
Threat model & preconditions
- Attacker position: local root (sysctl writes require
SYSCAP_NOSYSCTL_WRper kern_sysctl.c:1446 since the OID lacksCTLFLAG_ANYBODY). - Privileges gained or impact: controlled kernel heap corruption (slab-object overwrite), kernel OOB read into userspace (info leak), and/or panic. In a jail, capability mode, or any model where root is treated as a trust boundary below the kernel, this is root→kernel arbitrary write. Even in single-tenant use it is a trivial DoS.
- Required config or capabilities:
device hptmvloaded (the sysctl is always registered viahptregister_nodewhether or not hardware is present). - Reachability: sysctl
hptmv.statuswrite with a crafted 48-byteHPT_IOCTL_PARAM.
Proof of concept
PoC source: findings/poc/DF-1851/poc_hptmv_heapoverflow.c
Build & run
cc -o poc_hptmv_heapoverflow poc_hptmv_heapoverflow.c # As root: ./poc_hptmv_heapoverflow
Expected output
kernel: page fault trap, code=0 panic: vm_fault: ... (corrupted slab freelist metadata)
Immediate kernel panic from corrupted slab metadata on the copyin destination or a kfree corruption assert, proving the heap overflow.
Impact
High-severity root→kernel heap overflow. The integer overflow in the size check
is trivially exploitable: choose nInBufferSize and nOutBufferSize so their
32-bit sum wraps into [0, 4096]. The kmalloc allocates the wrapped size, but
copyin/copyout/Kernel_DeviceIoControl use the original sizes, producing
unbounded heap corruption.
Recommended fix
Bound each size individually before the sum, so the sum cannot wrap. Cast to
size_t to force 64-bit arithmetic.
--- a/sys/dev/raid/hptmv/hptproc.c
+++ b/sys/dev/raid/hptmv/hptproc.c
@@ -289,7 +289,10 @@ hpt_set_info(int length)
/*
* map buffer to kernel.
*/
- if (piop->nInBufferSize+piop->nOutBufferSize > PAGE_SIZE) {
+ if (piop->nInBufferSize > PAGE_SIZE ||
+ piop->nOutBufferSize > PAGE_SIZE ||
+ (size_t)piop->nInBufferSize + (size_t)piop->nOutBufferSize
+ > PAGE_SIZE) {
KdPrintE(("User buffer too large\n"));
return -EINVAL;
}
Also pass M_ZERO to the kmalloc (see DF-1852) and check the copyin return
value (see DF-1852).
References
- FreeBSD-SA-09:11.hptmv — same integer-overflow pattern in the FreeBSD hptmv driver.
DWORDtype: mvOs.h:62 (typedef unsigned int DWORD).HPT_IOCTL_PARAMlayout: hptintf.h:748-750.
Timeline
- 2026-07-20 Discovered during automated audit.
- 2026-07-20 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1851 · 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness reproducing the buggy arithmetic/control-flow | 2.3 KB | view raw |
| VERDICT.md | verdict | full verification narrative | 3.1 KB | ↓ raw |
| build.sh | build-script | exact build command | 88 B | view raw |
| run.sh | run-script | exact run invocation | 41 B | view raw |
| harness_run.log | run-log | harness output on guest | 452 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff | 1.3 KB | view raw |
| env.txt | environment | guest uname, cc version, kernel config | 768 B | view raw |
| README.md | readme | human-facing PoC README | 912 B | ↓ raw |
| poc_hptmv_heapoverflow.c | trigger-source | original PoC skeleton (pre-existing) | 2.0 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | ↓ download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1851 PoC
Trigger: write a crafted HPT_IOCTL_PARAM to sysctl hptmv.status
where nInBufferSize + nOutBufferSize wraps in 32-bit unsigned to a
value <= PAGE_SIZE, bypassing the size check. kmalloc allocates the
wrapped (small) size; copyin copies the original (huge) nInBufferSize
bytes, overflowing the slab.
Preconditions
device hptmvloaded (sysctl is always registered whether or not hardware is present).- Root (
SYSCAP_NOSYSCTL_WR).
Build
cc -o poc_hptmv_heapoverflow poc_hptmv_heapoverflow.c
Run
sudo ./poc_hptmv_heapoverflow
Expected output
kernel: page fault trap, code=0 panic: vm_fault: ... (corrupted slab freelist metadata)
Fix
See the finding markdown: bound each size individually before the sum
and cast to size_t to prevent 32-bit wraparound. Also add M_ZERO to
kmalloc (DF-1852) and check copyin return (DF-1852).
DF-1851 — Verification Verdict
Verdict: REPRODUCED (source-confirmed + arithmetic-harness)
The 32-bit integer overflow in the size-sum check is confirmed at
sys/dev/raid/hptmv/hptproc.c:292. The harness reproduces the
wraparound and shows the resulting 4095-byte heap overflow.
Mechanism
// hptproc.c:292 (under #ifdef SUPPORT_IOCTL, which IS defined — global.h:59)
if (piop->nInBufferSize + piop->nOutBufferSize > PAGE_SIZE) // 32-bit add
return -EINVAL;
// :297
ke_area = kmalloc(piop->nInBufferSize + piop->nOutBufferSize, M_DEVBUF, M_NOWAIT);
// :304
copyin(piop->lpInBuffer, ke_area, piop->nInBufferSize); // 4095-byte copyin
Both sizes are DWORD/unsigned int (hptintf.h:748,750) — the
addition wraps mod 2³². With nInBufferSize=4095, nOutBufferSize=0xFFFFF001,
the sum wraps to 0x100000001 mod 2³² = 1, which is <= PAGE_SIZE, so
the check is bypassed. kmalloc(1) returns a tiny slab; copyin then
copies 4095 bytes into it — a 4079-byte heap overflow. The subsequent
Kernel_DeviceIoControl(ke_area+nInBufferSize=ke_area+4095, ...)
writes OOB, and copyout on success leaks kernel heap.
This matches FreeBSD-SA-09:11.hptmv.
Harness evidence
DF-1851: hptproc.c:292 size check (32-bit unsigned add) nInBufferSize = 0x00000fff (4095) nOutBufferSize= 0xfffff001 (4294963201) 32-bit sum = 0x00000000 (0) sum > PAGE_SIZE (4096)? NO (passes — check bypassed) -> kmalloc(0) returns a 0-byte slab -> copyin(lpInBuffer, ke_area, 4095) overflows it by 4095 bytes This is the FreeBSD-SA-09:11.hptmv class: integer-overflow in size sum.
Why no live trigger on this guest (as unprivileged)
The hptmv.status sysctl IS registered on this guest (device hptmv is
in X86_64_GENERIC, and the sysctl handler runs at module init regardless
of HW). However, writing to sysctls requires SYSCAP_NOSYSCTL_WR
(kern_sysctl.c:1446) — maxx gets Operation not permitted. The bug
is therefore root-only on this guest: a root write to
hptmv.status with the crafted HPT_IOCTL_PARAM would trigger the
overflow (and then panic in Kernel_DeviceIoControl since
gIal_Adapter is NULL without HW).
This is a valid Phase-6 hard blocker for uid=0: the write is reachable
only from an already-root context, so there is no privilege boundary to
cross. (Root→kernel is game-over by definition.)
Exploit chain
Not applicable (root-only trigger — no unpriv→root boundary). The bug
is a real root→kernel heap-overflow primitive; on a host with actual
HPT RAID HW, gIal_Adapter is non-NULL so Kernel_DeviceIoControl
runs and the corruption is silently weaponizable rather than panicking.
PoC changes
- Added
harness.c: reproduces the 32-bit wrap arithmetic. - Added
fix.diff: per-field bounds +size_tcast +M_ZERO+ copyin return check.
Fix
fix.diff bounds each size individually before the sum, casts to
size_t before adding to prevent 32-bit wrap, adds M_ZERO to the
kmalloc, and checks the copyin return.
- BEFORE: harness shows sum wraps to 0, bypassing the check.
- AFTER: per-field bounds reject
nOutBufferSize > PAGE_SIZEbefore any addition.
Fix verification
fixedVALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: '32-bit sum = 0x00000000 ... passes — check bypassed' | patched: per-field bounds reject nOutBufferSize>PAGE_SIZE before addition
baseline (#0 unpatched): baseline harness: '32-bit sum = 0x00000000 ... passes — check bypassed' patched (#1 kernel, all 13 fixes, booted clean): patched: per-field bounds reject nOutBufferSize>PAGE_SIZE before addition kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- h
- p
- t
- p
- r
- o
- c
- .
- c
- :
- 2
- 9
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- h
- p
- t
- p
- r
- o
- c
- .
- c
- :
- 2
- 9
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- h
- p
- t
- p
- r
- o
- c
- .
- c
- :
- 3
- 0
- 4
Detail
Exploit chain
Root-only trigger (sysctl write requires SYSCAP_NOSYSCTL_WR; maxx gets EPERM). Valid Phase-6 hard blocker: write reachable only from already-root context, so no privilege boundary to cross. No uid=0 escalation claimed. Primitive characterized in harness.c (32-bit wrap arithmetic). Live ceiling: root->kernel heap overflow (game-over by definition); on host with real HPT HW, gIal_Adapter is non-NULL so Kernel_DeviceIoControl runs and corruption is silently weaponizable.
Evidence (decisive lines)
DF-1851: hptproc.c:292 size check (32-bit unsigned add) nInBufferSize = 0x00000fff (4095) nOutBufferSize= 0xfffff001 (4294963201) 32-bit sum = 0x00000000 (0) sum > PAGE_SIZE (4096)? NO (passes — check bypassed) -> kmalloc(0) returns a 0-byte slab
PoC changes
Added harness.c (32-bit wrap arithmetic) and fix.diff (per-field bounds + size_t cast + M_ZERO + copyin return check).
Verified recommended fix
fix.diff bounds each size individually before the sum, casts to size_t, adds M_ZERO, checks copyin return. supersedes finding proposal by also adding per-field bounds and copyin check.
Verdict
REPRODUCED at source+harness. hptproc.c:292 (under #ifdef SUPPORT_IOCTL which IS defined in global.h:59) does 'if (nInBufferSize+nOutBufferSize > PAGE_SIZE)' with both sizes DWORD (32-bit unsigned) — addition wraps mod 2^32. Harness with nIn=4095 nOut=0xFFFFF001 shows sum wraps to 0, bypasses check; kmalloc(0) then copyin(4095) -> 4095-byte heap overflow. FreeBSD-SA-09:11.hptmv class. The hptmv.status sysctl IS registered on this guest (device hptmv in GENERIC) but writing requires root (SYSCAP_NOSYSCTL_WR — maxx gets EPERM). Valid Phase-6 hard blocker for uid0: root-only trigger.
No comments yet.