ciss_notify_rescan_logical: unbounded ndrives + unchecked LUN_TO_BUS/TARGET -> OOB heap read and write
Summary
The notify-driven rescan computes ndrives from the controller-written list_size with NO CISS_MAX_LOGICAL clamp (the attach-time sibling checks it) and indexes sc->ciss_logical[bus][target] from CISS_LUN_TO_BUS/TARGET (0..255 each) with no bounds check. cll->lun[i] is read past the 15-LUN kmalloc'd report buffer, and the OOB ld is written (cl_update/cl_address/cl_controller plus identify-time cl_ldrive/cl_lstatus pointer stores). VERIFIED: redzone-backed replica - 226 OOB reads (bus=255 target=255) and 226 OOB index computations; -DFIXED (clamp+bounds) 0 OOB accesses. Firmware-controlled heap OOB read feeding arbitrary-offset OOB writes relative to the ciss_logical row array on any logical-drive status-change/media-exchange event (MSA failover, drive insertion, firmware rescan). Upstream-inherited; the notify/rescan sibling the pass-1 set (DF-1190) missed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3729 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | faithful userspace replica of the cited kernel path (real macros/struct constants) | 5.8 KB | view raw |
| build.sh | build-script | cc -O2 (and -DFIXED replica) | 65 B | view raw |
| run.sh | run-script | baseline + fixed runs with exit codes | 89 B | view raw |
| build.log | build-log | full untrimmed guest compiler output | 43 B | view raw |
| run.log | run-log | decisive run: baseline [BUG REPRODUCED] + fixed [FIXED] | 28.9 KB | view raw |
| run.2.log | run-log | repeat run (stability) | 28.9 KB | view raw |
| run.3.log | run-log | repeat run (stability) | 28.9 KB | view raw |
| fix_run.log | run-log | -DFIXED section (persist_fix reads this) | 952 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, path:line, harness proof, fix | 3.9 KB | β raw |
| fix.diff | suggested-fix | clamp ndrives to CISS_MAX_LOGICAL and bounds-check bus/target in ciss_notify_rescan_logical (mirror ciss.c:1357) | 1.1 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run on guest /usr/src (clean) | 2.8 KB | view raw |
| fix_build.log | build-log | ciss.ko rebuilt with all 7 pass-2 fixes at -Werror, link rc=0 | 1.0 KB | view raw |
| env.txt | environment | guest uname, no-HW proof, module presence | 538 B | view raw |
| README.md | readme | human reproduce doc | 2.0 KB | β raw |
| manifest.json | manifest | this file | 3.0 KB | view raw |
| verdict.json | verdict | machine verdict (persist_poc.py schema) | 2.6 KB | view raw |
DF-3729 β ciss_notify_rescan_logical: unbounded ndrives + unchecked LUN_TO_BUS/TARGET β OOB heap read and write
File: sys/dev/raid/ciss/ciss.c:3866 (ndrives from the
controller-written list_size, no CISS_MAX_LOGICAL sanity check β
unlike ciss_init_logical at ciss.c:1357) and ciss.c:3896-3912
(cll->lun[i] read past the 15-LUN report buffer; ciss_logical[bus][target]
indexed with busβ[0,255] / targetβ[0,255] against rows=
ciss_max_logical_bus (typically 1-2) Γ columns CISS_MAX_LOGICAL=15).
Class: kernel heap OOB read feeding OOB writes (CWE-125/CWE-787), firmware-data-driven. Upstream-inherited (FreeBSD has the same walk); the pass-1 set (DF-1190) fixed only the attach-time sibling.
Reproduce
./build.sh && ./run.sh
Expected output
Baseline: 226 OOB reads past the report buffer and 226 OOB row/col index
computations, [BUG REPRODUCED]. Fixed replica (-DFIXED = clamp ndrives
to CISS_MAX_LOGICAL + validate bus/target): [FIXED] ... 0 OOB accesses.
Trigger (kernel)
A CISS_NOTIFY_LOGICAL/status-change (or media-exchange) event makes
ciss_notify_logical call ciss_notify_rescan_logical (ciss.c:3960/3973),
which issues REPORT_LOGICAL_LUNS into a 15-LUN buffer and walks the
firmware-supplied list_size. A malicious or buggy controller reply
(list_size larger than the buffer, and/or LUN addresses encoding bus/target
outside the arrays β cissreg.h:509-510) produces the OOB accesses; the
driver then WRITES through the OOB ld (cl_update, cl_address,
cl_controller; ciss_identify_logical stores kmalloc pointers into
cl_ldrive/cl_lstatus).
Fix
fix.diff (two hunks: clamp + bounds check, mirroring ciss.c:1357's
established pattern). Compile-validated with the other 6 ciss fixes at
-Werror (fix_build.log); behaviorally validated by the -DFIXED run.
Artifacts
VERDICT.md, harness.c, build.sh, run.sh, build.log, run.log,
run.2.log, run.3.log, env.txt, fix.diff, fix_apply.log,
fix_build.log, manifest.json, verdict.json.
DF-3729 VERDICT
Finding
ciss_notify_rescan_logical() (sys/dev/raid/ciss/ciss.c:3851-3914) is the
event-driven sibling of ciss_init_logical(). Where the attach-time path
validates the controller reply:
/* ciss.c:1356-1362 (init_logical β HAS the check) */
ndrives = (ntohl(cll->list_size) / sizeof(union ciss_device_address));
if ((ndrives < 0) || (ndrives > CISS_MAX_LOGICAL)) { ... ENXIO }
the notify path has none of it:
/* ciss.c:3866 (notify_rescan_logical β NO check) */
ndrives = (ntohl(cll->list_size) / sizeof(union ciss_device_address));
...
/* ciss.c:3896-3912 */
for (i = 0; i < ndrives; i++) {
bus = CISS_LUN_TO_BUS(cll->lun[i].logical.lun); /* 0..255 */
target = CISS_LUN_TO_TARGET(cll->lun[i].logical.lun); /* 0..255 */
ld = &sc->ciss_logical[bus][target]; /* rows: ciss_max_logical_bus
(min 2), cols: 15 */
...
ld->cl_update = 0; /* OOB WRITE */
ld->cl_address = cll->lun[i]; /* OOB WRITE */
ld->cl_controller = &sc->ciss_controllers[bus]; /* OOB read + OOB store */
if (ciss_identify_logical(sc, ld) == 0) /* stores kmalloc'd
cl_ldrive/cl_lstatus
pointers OOB */
cll was kmalloc'd for exactly sizeof(*cll) + CISS_MAX_LOGICAL*8 bytes
(ciss.c:1270 via ciss_report_luns(..., CISS_MAX_LOGICAL)), so a firmware
list_size claiming more than 15 LUNs makes cll->lun[i] an OOB heap
read past the allocation, and the derived bus/target (cissreg.h:509-510)
index ciss_logical up to 255 rows / 255 columns out of bounds β OOB
writes at 2 * sizeof(void*)-ish granularity plus ciss_identify_logical's
pointer stores β an arbitrary-offset heap write primitive relative to the
ciss_logical row array, fully firmware-chosen.
FreeBSD's ciss_notify_rescan_logical (upstream ciss.c:3967-4030) has the
identical unbounded walk β this is an upstream-inherited defect; the pass-1
audit (DF-1190) fixed only the attach-time copy of the pattern.
Reachability
ciss_notify_logical (ciss.c:3960, 3973) calls this on every
logical-drive status-change / media-exchange notify event, i.e. whenever the
adapter reconfigures volumes β remote/MSA failover, drive insertion,
firmware-initiated rescan. The notify event data itself is
firmware-supplied (and its bus/target are separately unbounded β that
part is the known DF-1189); this finding is the rescan table walk the
known set missed.
Verification (no HP Smart Array on the audit guest)
harness.c replicates the walk with a redzone-backed 128-byte report buffer
and a 2-row/15-col logical table:
- baseline (run.log, stable in run.2/run.3): with list_size = 0x1000
(4096 LUNs claimed), iterations β₯15 read the redzone (raw 0xffffffff)
and compute bus=255 target=255 β 226 OOB reads + 226 OOB index
computations β [BUG REPRODUCED];
- -DFIXED replica (clamp + bounds check): [FIXED] ... 0 OOB accesses.
Fix validation
fix.diff: hunk 1 clampsndrivesexactly like ciss.c:1357 (plus freesclland returns); hunk 2 rejects out-of-range bus/target entries.fix_apply.log: clean dry-run;fix_build.log:ciss.korebuilt with all 7 ciss pass-2 fixes at-Werror, link rc=0.- Harness
-DFIXEDrun: 0 OOB accesses.
Bottom line: reproduced (harness-level, firmware-gated path); unbounded firmware-controlled heap read + write in the notify rescan walk; fix validated at compile + replica level.
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
fixedTwo hunks: clamp ndrives like ciss.c:1357 (free+return on absurd count) and reject bus/target outside ciss_max_logical_bus/CISS_MAX_LOGICAL. -Werror module build + -DFIXED replica clean (0 OOB).
fix.diff (2 hunks); fix_apply.log; fix_build.log; fix_run.log [FIXED] 0 OOB accesses
Confirmed kernel references
Detail
Exploit chain
logical status-change notify -> rescan REPORT_LOGICAL_LUNS -> forged list_size/lun[] -> cll->lun[i] OOB read -> ciss_logical[bus][target] OOB writes
Evidence (decisive lines)
run.log: 226 OOB redzone reads (raw 0xffffffff -> bus=255 target=255) + 226 OOB index computations, [BUG REPRODUCED]; fix_run.log [FIXED] 0 OOB accesses
PoC changes
harness written fresh; loop capped at 256 iterations for demo runtime (kernel loop is fully unbounded)
Verified recommended fix
clamp ndrives to CISS_MAX_LOGICAL and bounds-check bus/target in ciss_notify_rescan_logical (mirror ciss.c:1357)
Verdict
REPRODUCED (harness-level; firmware-gated path): ciss_notify_rescan_logical computes ndrives from the controller-written list_size with NO CISS_MAX_LOGICAL clamp (ciss.c:3866; the attach-time sibling checks it at ciss.c:1357) and indexes ciss_logical[bus][target] from CISS_LUN_TO_BUS/TARGET with no bounds check (ciss.c:3899-3901). Firmware-controlled heap OOB READ past the 15-LUN report buffer plus OOB WRITES (cl_update/cl_address/cl_controller and identify-time cl_ldrive/cl_lstatus pointer stores) up to 255 rows/cols out of range. Upstream-inherited; the pass-1 set (DF-1190) fixed only the attach-time copy.
No comments yet.