RIO1 fast-post loop trusts card-controlled 8-bit rqs_seqno against a 15-element handle array β kernel stack OOB read up to 960 bytes
Summary
isp_intr()'s RQSTYPE_RIO1 handler loops 'for (i = 0; i < rio->req_header.rqs_seqno; i++) isp_fastpost_complete(isp, rio->req_handles[i]);' where isp_rio1_t is exactly 64 bytes: 4-byte header + uint32_t req_handles[15]. rqs_seqno is a full card/DMA-written byte (0..255) with no clamp, so seqno > 15 walks the kernel stack up to 960 bytes past the 64-byte buffer; the garbage words are consumed as completion handles (validated by ISP_VALID_INI_HANDLE - lookup keys only, chance spurious command completion). RIO 32-bit is enabled by default on Ultra3 SCSI cards. Faulty/malicious HBA firmware (card-as-trust-boundary). PROOF: bounds harness vs verbatim headers - 'FAULT (loop reached i == 16, array holds 15)' observed twice. Inconclusive (no HBA). Fix: clamp to NELEM(req_handles).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3583 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger_df3583.c | β | 3.3 KB | view raw | |
| build.sh | β | 214 B | view raw | |
| run.sh | β | 97 B | view raw | |
| build.log | β | 1.2 KB | view raw | |
| run.log | β | 2.5 KB | view raw | |
| VERDICT.md | β | 2.8 KB | β raw | |
| verdict.json | β | 2.8 KB | view raw | |
| fix.diff | β | 665 B | view raw | |
| manifest.json | β | 950 B | view raw |
DF-3583 β Unbounded card-controlled handle count in RIO1 fast-post loop (kernel stack OOB read)
WHAT sys/dev/disk/isp/isp.c:5087-5095, inside isp_intr()'s response-queue loop:
} else if (etype == RQSTYPE_RIO1) {
isp_rio1_t *rio = (isp_rio1_t *) qe; /* uint8_t qe[QENTRY_LEN] */
isp_get_rio1(isp, (isp_rio1_t *) hp, rio);
for (i = 0; i < rio->req_header.rqs_seqno; i++) {
isp_fastpost_complete(isp, rio->req_handles[i]); /* OOB */
}
isp_rio1_t (ispmbox.h:866-871) is exactly QENTRY_LEN = 64 bytes:
a 4-byte isphdr_t plus uint32_t req_handles[15]. rqs_seqno is a full
8-bit byte written by the card (DMA); nothing clamps it to 15. A card
claiming N > 15 handles makes the loop read (N-15)*4 bytes past the
64-byte kernel-stack buffer β up to 960 bytes of adjacent stack walked,
with each garbage word consumed as a completion handle
(isp_fastpost_complete -> isp_find_xs validates, so the values act only
as lookup keys; a chance match spuriously completes/destroys that
command).
REACHABILITY RIO1 entries only occur when the driver negotiates FW_FEATURE_RIO_32BIT, which the default build does for Ultra3 SCSI cards (isp.c:1360-1361, IS_ULTRA3, ISP_NO_RIO not defined). Requires faulty or malicious HBA firmware to set seqno > 15 β the card is the trust boundary here; this is a robustness defect against bad hardware, not a remotely reachable bug.
PROOF (this pack) trigger_df3583.c runs the identical loop with seqno = 0xff against qe[64] placed flush against a PROT_NONE guard page:
FAULT (SIGSEGV/SEGV) at 0x80047d000 => 0 bytes past the end of qe[64]
(loop reached i == 16, array holds 15)
i.e. the very first out-of-array read (i == 16) already leaves the
buffer; with seqno = 0xff the kernel-side loop would keep reading to
qe+1024, 960 bytes past the buffer end.
WHY "inconclusive" Same reason as DF-3582: no QLogic HBA in the guest, so the in-kernel path cannot be exercised. The defect and its bounds are proven from the real kernel headers on the guest compiler.
UPSTREAM Modern FreeBSD removed the RIO1 handling from isp_intr() entirely; the unclamped loop is inherited from the 2011 FreeBSD revision DragonFly carries.
FIX fix.diff β clamp the loop to the array size.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff clamps the loop with NELEM(rio->req_handles); NELEM is already used in this file (isp.c:7163). Baseline not reproducible without hardware, so no patched-kernel A/B was possible; the clamp is a pure bounds correction.
fix.diff (git-apply-able against sys/dev/disk/isp/isp.c hunk at line 5093)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log β 'sizeof(isp_rio1_t) = 64 (header 4 + req_handles[15])' and 'FAULT (SIGSEGV/SEGV) ... (loop reached i == 16, array holds 15)'; trigger_df3583.c; VERDICT.md; fix.diff (clamp to NELEM(req_handles))
PoC changes
No seed (pass-2 new finding). Trigger written from scratch against the verbatim kernel headers; qe[64] placed flush against a PROT_NONE page so the first out-of-array read (i == 16) faults deterministically.
Verified recommended fix
Clamp the RIO1 fast-post loop to NELEM(rio->req_handles) entries.
Verdict
Kernel stack OOB read confirmed at source level and proven arithmetically on the guest with the real kernel headers: isp_intr()'s RQSTYPE_RIO1 handler (sys/dev/disk/isp/isp.c:5093) loops i < rio->req_header.rqs_seqno over isp_rio1_t.req_handles[15] (ispmbox.h:866-871) where rqs_seqno is a full card-written 8-bit byte; seqno > 15 reads up to 960 bytes past the 64-byte stack buffer qe[]. The extracted-logic PoC faults at the first i == 16 iteration (run.log). Values are only used as completion-handle lookup keys (isp_fastpost_complete validates via ISP_VALID_INI_HANDLE), so impact is chance spurious command completion/stack-value disclosure into lookup keys β not a direct user-visible leak. Reachable only on Ultra3 SCSI cards (RIO 32-bit enabled by default, isp.c:1360-1361) with faulty/malicious firmware; guest has no QLogic HBA so the in-kernel path was not exercised. Modern FreeBSD removed RIO1 handling entirely.
No comments yet.