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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3583 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-3583

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.