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

Heap overflow via unbounded CAM scatter-gather count in hpt_scsi_io

  • File: sys/dev/raid/hpt27xx/hpt27xx_osm_bsd.c
  • Lines: 715, 717, 724, 728, 479, 488
  • 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

hpt_scsi_io copies ccb->csio.sglist_cnt scatter/gather entries from a caller-supplied list into pCmd->psg without any bounds check.

pCmd->psg aliases ext->psg, a fixed array of os_max_sg_descriptors (==18) SG entries (os_bsd.h:155, osm.h:40).

Since sglist_cnt is a u_int16_t (cam_ccb.h:604) reaching up to 65535, a caller-supplied CCB with sglist_cnt > 18 writes attacker-controlled 16-byte SG records (size/eot/addr) past the end of the OS_CMDEXT structure into adjacent kernel heap.

The same unbounded loop is duplicated in os_buildsgl (lines 479-488).

This is the same bug class as DF-1311 (hptiop) and DF-1529-1531 (hptmv).

Root cause

At hpt27xx_osm_bsd.c:715 pCmd->psg = ext->psg; aliases the command's S/G pointer to a fixed 18-entry array declared in os_bsd.h:155 as SG psg[os_max_sg_descriptors]; with os_max_sg_descriptors==18 (osm.h:40).

The loop at lines 724-728:

for (idx = 0; idx < ccb->csio.sglist_cnt; idx++) {
    pCmd->psg[idx].addr.bus = sgList[idx].ds_addr;
    pCmd->psg[idx].size = sgList[idx].ds_len;
    pCmd->psg[idx].eot = ...;
}

uses the CCB's sglist_cnt (u_int16_t per cam_ccb.h:604) as the only bound.

No comparison to os_max_sg_descriptors (or to the dma tag's nsegments=18 set at line 1047) exists anywhere on the CAM_SCATTER_VALID path.

sizeof(SG)=16 on 64-bit (him.h:281-290: HPT_U32 size + pad + HPT_UINT eot + 8-byte addr union), so sglist_cnt=N>18 overflows by (N-18)*16 bytes.

psg is the LAST field of OS_CMDEXT, so the overflow runs straight into whatever follows in M_DEVBUF.

os_buildsgl() at lines 479-488 has the identical defect against the same ext->psg alias when called by LDM with logical=TRUE.

Threat

Attacker must be able to issue a CAM pass-through CCB to an hpt27xx-managed target id (0..osm_max_targets-1).

On DragonFly this requires opening /dev/passN which is created mode 0600 root (scsi_pass.c:279-280), so the live trigger is a privileged user β€” but this is exactly the trust boundary that protects kernel heap integrity (root inside a jail, defense-in-depth against privileged-but-untrusted code, and any future relaxation of devfs rules).

With a malicious CCB the attacker obtains an arbitrary-length, fully attacker-controlled 16-byte-aligned overwrite of kernel heap immediately following an OS_CMDEXT object (allocated in batches of os_max_queue_comm==32 at lines 1055-1069, so the next allocation is typically another OS_CMDEXT β€” corrupting its vbus_ext/next/ccb/dma_map/psg fields).

Impact ranges from trivial DoS (panic on corrupted freelist or invalid ext->ccb deref in os_cmddone at line 424) to kernel arbitrary write / code execution if the heap is groomed so the overwrite lands on a victim object containing a function pointer or pointer-to-function dereference (e.g., the done/buildsgl/target fields of a PCOMMAND).

Validate ccb->csio.sglist_cnt against os_max_sg_descriptors before the loop. Apply at both the inline copy site in hpt_scsi_io and the helper os_buildsgl:

--- a/sys/dev/raid/hpt27xx/hpt27xx_osm_bsd.c
+++ b/sys/dev/raid/hpt27xx/hpt27xx_osm_bsd.c
@@ -714,6 +714,13 @@ static void hpt_scsi_io(PVBUS_EXT vbus_ext, union ccb *ccb)

        if (ccb->ccb_h.flags & CAM_SCATTER_VALID) {
            int idx;
+           if (ccb->csio.sglist_cnt == 0 ||
+               ccb->csio.sglist_cnt > os_max_sg_descriptors) {
+               cmdext_put(ext);
+               ldm_free_cmds(pCmd);
+               ccb->ccb_h.status = CAM_REQ_INVALID;
+               xpt_done(ccb);
+               return;
+           }
            bus_dma_segment_t *sgList = (bus_dma_segment_t *)ccb->csio.data_ptr;

            if (ccb->ccb_h.flags & CAM_SG_LIST_PHYS)
@@ -478,6 +485,11 @@ static int os_buildsgl(PCOMMAND pCmd, PSG pSg, int logical)
            if (ccb->ccb_h.flags & CAM_SG_LIST_PHYS)
                panic("physical address unsupported");

+           if (ccb->csio.sglist_cnt > os_max_sg_descriptors)
+               return FALSE;
+
            for (idx = 0; idx < ccb->csio.sglist_cnt; idx++) {
                os_set_sgptr(&pSg[idx], (HPT_U8 *)(HPT_UPTR)sgList[idx].ds_addr);

The proper long-term fix is to publish os_max_sg_descriptors (or a new maxsg field) via XPT_PATH_INQ so CAM enforces it at the periph layer, but the SIM must not rely on that alone.

  • DF-1311 (twin, hptiop): unbounded SG list stack overflow.
  • DF-1529-1531 (siblings, hptmv): HPT GUI ioctl family.
  • DF-1487 (twin, if_wb): TX fragment array OOB.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1563 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source replicates for(idx<sglist_cnt) psg[idx]=... overflow 4.4 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 (2 format warnings, harmless) 490 B view raw
run.log run-log decisive run; 6/10 sglist_cnt values overflow 1.4 KB view raw
env.txt environment uname + guest PCI inventory (no hpt27xx) 543 B view raw
fix.diff suggested-fix reject sglist_cnt > os_max_sg_descriptors before loop 927 B view raw
fix_build.log fix-build-log patched nativekernel, rc=0 5.6 MB ↓ download
VERDICT.md verdict full narrative 3.8 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-1563 β€” hpt27xx sglist_cnt heap overflow

Verdict

REPRODUCED (source-level harness). The bug is real; impact ceiling is a heap overflow of (sglist_cnt - 18) * 16 bytes past OS_CMDEXT.psg into the adjacent M_DEVBUF slab allocation β€” up to ~1 MiB with sglist_cnt = 65535. The kernel path requires an hpt27xx HBA plus a /dev/passN for one of its targets, neither of which exists on the audit guest. Harness demonstrates the genuine loop and overflow magnitude. fix.diff applies cleanly and nativekernel succeeds (rc=0).

Mechanism (sys/dev/raid/hpt27xx/hpt27xx_osm_bsd.c)

  1. Line 715: pCmd->psg = ext->psg; β€” ext->psg aliases to a fixed SG psg[os_max_sg_descriptors] array (os_bsd.h:155), where os_max_sg_descriptors = 18 (osm.h:40).
  2. Lines 717-728: when CAM_SCATTER_VALID is set, the loop for (idx = 0; idx < ccb->csio.sglist_cnt; idx++) pCmd->psg[idx] = ... iterates the user-supplied sglist_cnt.
  3. ccb->csio.sglist_cnt is u_int16_t (cam_ccb.h:604), max 65535 β€” there is no bound check against os_max_sg_descriptors (18).
  4. Each SG entry is 16 bytes on amd64 (sizeof(SG) = 16, harness-measured).
  5. psg is the last field of OS_CMDEXT (os_bsd.h:155-157), so the overflow runs straight into the next M_DEVBUF allocation β€” no padding, no sentinel.
  6. With sglist_cnt = 64, the loop writes 736 bytes past OS_CMDEXT; with sglist_cnt = 65535, it writes ~1 MiB.

os_buildsgl at lines 479-488 has the same defect (separate code path).

Harness proof (harness.c)

Replicates the genuine for(idx=0; idx<sglist_cnt; idx++) pCmd->psg[idx]=... loop and reports overflow for representative sglist_cnt values:

sizeof(SG) = 16
sizeof(OS_CMDEXT) = 320
os_max_sg_descriptors = 18

sglist_cnt            bytes-written          buffer-size                  OOB
18                              288                  288                    0
19                              304                  288                   16
32                              512                  288                  224
64                             1024                  288                  736
256                            4096                  288                 3808
1024                          16384                  288                16096
65535                       1048560                  288              1048272

Overflowing sglist_cnt values: 6/10
psg[18] write offset = 320 (== sizeof(OS_CMDEXT))

Exploit-chain note

Trigger requires an hpt27xx SIM and /dev/passN access (root or operator group). With those, this is a fully-attacker-controlled heap-overflow primitive (every written byte is from the user-supplied SG list). On a host with the HBA this is a credible root→kernel-code-exec primitive via heap grooming into a victim object with a function pointer. Documented as primitive characterization; impact ceiling is heap corruption.

PoC changes

  • Original folder was README only.
  • Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Fix

fix.diff adds an explicit sglist_cnt > os_max_sg_descriptors reject before the loop, freeing resources and completing the CCB with CAM_REQ_CMP_ERR. Matches the finding markdown proposal ("validate sglist_cnt <= os_max_sg_descriptors"). A sibling fix is needed for the os_buildsgl path at lines 479-488 (the HPT_ASSERT(nsegs<= os_max_sg_descriptors) at line 514 already covers that path on INVARIANTS kernels).

Fix-validation

patch -p1 --forward succeeds (hunk #1 at line 721). nativekernel completes with rc=0 (fix_build.log). No run-time exercise possible because no hpt27xx HBA / no /dev/passN on the guest β†’ fix_status: "not_testable". Diff applies and compiles; changed logic rejects oversized sglist_cnt.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because no hpt27xx HBA / no /dev/passN on the audit guest; validated that fix.diff applies cleanly (hunk #1 at line 721) and single-fix nativekernel compiles rc=0 (fix_build.log).

baseline (harness): Overflowing sglist_cnt values: 6/10
patched kernel build: === NK_DONE rc=0 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1563-fix (single-fix kernel built, rc=0)

Confirmed kernel references

Detail

Exploit chain

none (HW-gated): no hpt27xx HBA in QEMU and trigger requires /dev/passN. Primitive characterized via harness: fully-attacker-controlled (every byte is from user SG list) heap overflow of (sglist_cnt-18)*16 bytes into adjacent M_DEVBUF slab. Realistic ceiling on a host with the HBA: root/operator -> kernel-code-exec via heap grooming into a victim object with a function pointer.

Evidence (decisive lines)

sizeof(SG) = 16
sizeof(OS_CMDEXT) = 320
sglist_cnt            bytes-written          buffer-size                  OOB
19                              304                  288                   16
32                              512                  288                  224
64                             1024                  288                  736
256                            4096                  288                 3808
1024                          16384                  288                16096
65535                       1048560                  288              1048272
Overflowing sglist_cnt values: 6/10
psg[18] write offset = 320 (== sizeof(OS_CMDEXT))

PoC changes

Original folder was README only. Added harness.c replicating for(idx<sglist_cnt) psg[idx]=... overflow, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Verified recommended fix

fix.diff adds an explicit sglist_cnt > os_max_sg_descriptors reject before the loop (freeing resources, CAM_REQ_CMP_ERR, xpt_done, return). Matches finding markdown proposal. A sibling fix is needed for the os_buildsgl path (lines 479-488); the existing HPT_ASSERT(nsegs<=os_max_sg_descriptors) at line 514 already covers that path on INVARIANTS kernels.

Verdict

REPRODUCED at the source-logic level. hpt27xx_osm_bsd.c:715 pCmd->psg = ext->psg aliases to a fixed SG psg[os_max_sg_descriptors=18] array (os_bsd.h:155, osm.h:40). Lines 724-728 for(idx=0; idxcsio.sglist_cnt; idx++) pCmd->psg[idx]=... iterates the user-supplied sglist_cnt (u16, max 65535) with NO bound check vs 18. sizeof(SG)=16 bytes; psg is the LAST field of OS_CMDEXT (sizeof=320), so overflow runs straight into the adjacent M_DEVBUF slab. Harness confirms 6/10 sglist_cnt values overflow; sglist_cnt=65535 -> 1048272-byte overflow. No hpt27xx HBA / no /dev/passN on guest; harness proof only.