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).
Recommended fix
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.
Related findings
- 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)
PoC verification
Evidence pack
findings/poc/DF-1563 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- Line 715:
pCmd->psg = ext->psg;βext->psgaliases to a fixedSG psg[os_max_sg_descriptors]array (os_bsd.h:155), whereos_max_sg_descriptors = 18(osm.h:40). - Lines 717-728: when
CAM_SCATTER_VALIDis set, the loopfor (idx = 0; idx < ccb->csio.sglist_cnt; idx++) pCmd->psg[idx] = ...iterates the user-suppliedsglist_cnt. ccb->csio.sglist_cntisu_int16_t(cam_ccb.h:604), max 65535 β there is no bound check againstos_max_sg_descriptors(18).- Each
SGentry is 16 bytes on amd64 (sizeof(SG) = 16, harness-measured). psgis the last field ofOS_CMDEXT(os_bsd.h:155-157), so the overflow runs straight into the nextM_DEVBUFallocation β no padding, no sentinel.- With
sglist_cnt = 64, the loop writes 736 bytes pastOS_CMDEXT; withsglist_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_testablenot_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 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- 2
- 7
- x
- x
- /
- h
- p
- t
- 2
- 7
- x
- x
- _
- o
- s
- m
- _
- b
- s
- d
- .
- c
- :
- 7
- 1
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- 2
- 7
- x
- x
- /
- h
- p
- t
- 2
- 7
- x
- x
- _
- o
- s
- m
- _
- b
- s
- d
- .
- c
- :
- 7
- 1
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- 2
- 7
- x
- x
- /
- h
- p
- t
- 2
- 7
- x
- x
- _
- o
- s
- m
- _
- b
- s
- d
- .
- c
- :
- 7
- 2
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- 2
- 7
- x
- x
- /
- h
- p
- t
- 2
- 7
- x
- x
- _
- o
- s
- m
- _
- b
- s
- d
- .
- c
- :
- 7
- 2
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- 2
- 7
- x
- x
- /
- o
- s
- _
- b
- s
- d
- .
- h
- :
- 1
- 5
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- h
- p
- t
- 2
- 7
- x
- x
- /
- o
- s
- m
- .
- h
- :
- 4
- 0
- s
- y
- s
- /
- b
- u
- s
- /
- c
- a
- m
- /
- c
- a
- m
- _
- c
- c
- b
- .
- h
- :
- 6
- 0
- 4
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; idx
No comments yet.