isp_control() ISPCTL_RESET_DEV/ABORT_CMD index fcp->isp_dev_map[tgt] without validating tgt; the CAM glue packs bus<<16 into tgt so every channel>0 device reset reads ~108 KB out of bounds
Summary
Both ISPCTL paths perform 'hdlidx = fcp->isp_dev_map[tgt] - 1;' bounds-checking only the loaded value, never the index - unlike isp_start() which validates the target before the same array. The XPT_RESET_DEV glue ORs the bus number into the target ('tgt |= (bus << 16)'), so for a device on channel 1 of a multi-channel 24xx-class card the load is at base+151608 against a 43152-byte 2-channel allocation - 108,456 bytes out of bounds: a wild kernel read during BDR handling (panic where unmapped, garbage index otherwise). Local DoS on multi-channel ISP FC hardware: an operator-group user via pass(4) camcontrol reset, or plain CAM error recovery on a hung command on port B, deterministically performs the wild read. PROOF: index-arithmetic harness vs verbatim headers - '=> 108456 bytes past allocation end' observed. Inconclusive (no HBA). Fix: validate tgt < MAX_FC_TARG before both loads (+ drop the bus<<16 packing in the glue).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3584 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger_df3584.c | β | 3.8 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.9 KB | β raw | |
| verdict.json | β | 3.4 KB | view raw | |
| fix.diff | β | 890 B | view raw | |
| manifest.json | β | 1018 B | view raw |
DF-3584 β Unvalidated target index into fcp->isp_dev_map[] in isp_control() (far OOB read)
WHAT sys/dev/disk/isp/isp.c:4522 (ISPCTL_RESET_DEV) and 4624 (ISPCTL_ABORT_CMD), inside isp_control():
hdlidx = fcp->isp_dev_map[tgt] - 1;
if (hdlidx < 0 || hdlidx >= MAX_FC_TARG) { ... }
The RESULT of the array load is bounds-checked, but `tgt` β the INDEX β
is never validated against MAX_FC_TARG (contrast isp_start(),
isp.c:4247-4254, which checks XS_TGT before the same array).
The CAM glue makes it worse: XPT_RESET_DEV packs the bus number into the
same word (isp_freebsd.c:4519-4523):
bus = cam_sim_bus(xpt_path_sim(ccb->ccb_h.path));
tgt = ccb->ccb_h.target_id;
tgt |= (bus << 16);
error = isp_control(isp, ISPCTL_RESET_DEV, bus, tgt);
so for a device on channel 1 the 24XX path reads
offsetof(fcparam, isp_dev_map) + 2*(65536 + target_id)
= 20536 + 131072 = >=151608 bytes into an allocation of
nchan*sizeof(fcparam) (21576 or 43152 bytes)
β >= 108 KB past the end: a wild kernel read on EVERY BDR issued to a
channel>0 device (panic if the page is unmapped; garbage index otherwise,
which the post-check then mostly rejects).
REACHABILITY Any XPT_RESET_DEV reaching the driver for a channel>0 FC device: cam_periph error recovery (a hung/errored disk on port B of a dual-port 24xx/25xx, NPIV/multi-ID configurations), or camcontrol reset issued by an operator-group user via the pass(4) node. The read is deterministic; whether it faults depends on what follows the allocation in kmem.
PROOF (this pack) trigger_df3584.c performs the identical isp_dev_map[tgt] load with tgt = 0x10000 (bus 1, target 0) against a 43152-byte (2-channel) fcparam-sized allocation followed by PROT_NONE:
read address = base + 20536 + 2*65536 = base + 151608
=> 108456 bytes past the end of the 43152-byte allocation
FAULT (SIGSEGV/SEGV) at 0x8004c5038 => 108456 bytes past allocation end
WHY "inconclusive" No QLogic HBA (and in particular no multi-channel 24xx) in the guest, so the in-kernel path cannot be exercised. Offsets proven with the real kernel headers on the guest compiler.
FIX
fix.diff β validate tgt before indexing (both ISPCTL_RESET_DEV and
ISPCTL_ABORT_CMD), mirroring isp_start()'s check. (The tgt |= bus<<16
packing in isp_freebsd.c should also be dropped by whoever owns that
file; the isp.c-side validation makes the core robust regardless.)
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 adds the same MAX_FC_TARG bounds check isp_start() already performs, before the isp_dev_map[] load, in both ISPCTL_RESET_DEV and ISPCTL_ABORT_CMD. Baseline not reproducible without hardware; no patched-kernel A/B possible. Callers should additionally stop packing bus<<16 into tgt (isp_freebsd.c), but the isp.c-side check removes the memory-safety hazard regardless.
fix.diff (git-apply-able against sys/dev/disk/isp/isp.c hunks at lines 4522 and 4624)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log β 'read address = base + 20536 + 2*65536 = base + 151608', '=> 108456 bytes past the end of the 43152-byte allocation', 'FAULT (SIGSEGV/SEGV) at 0x8004c5038 => 108456 bytes past allocation end'; trigger_df3584.c; VERDICT.md; fix.diff (bounds-check tgt in both ISPCTL cases)
PoC changes
No seed (pass-2 new finding). Trigger written from scratch against verbatim kernel headers; one iteration needed a missing
Verified recommended fix
Bounds-check tgt (0 <= tgt < MAX_FC_TARG) before indexing fcp->isp_dev_map[] in ISPCTL_RESET_DEV/ISPCTL_ABORT_CMD.
Verdict
Unchecked array index confirmed at source level: isp_control() ISPCTL_RESET_DEV (sys/dev/disk/isp/isp.c:4522) and ISPCTL_ABORT_CMD (isp.c:4624) read fcp->isp_dev_map[tgt] without validating tgt against MAX_FC_TARG, unlike isp_start() (isp.c:4247-4254) which checks the same mapping. The CAM XPT_RESET_DEV glue packs the bus number into tgt (isp_freebsd.c:4519-4523 'tgt |= (bus << 16)'), so every BDR for a channel>0 FC device indexes at least 65536 entries past the array: read address base+151608 vs a 43152-byte 2-channel allocation β 108,456 bytes out of bounds (fault proven by the extracted-logic PoC, run.log). In-kernel impact is a wild read during BDR handling: kernel panic where unmapped, otherwise a garbage index that the existing post-check usually rejects. Not exercised on the guest (no QLogic HBA / no multi-channel card). Additionally observed while tracing the caller: DragonFly's XPT_ABORT passes the XPT_ABORT CCB itself instead of the CCB to abort (isp_freebsd.c:4541 'isp_control(isp, ISPCTL_RESET_DEV...' vs upstream's accb), making command abort always fail the handle lookup β a functional divergence in the neighboring file, noted for that file's audit.
No comments yet.