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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3729 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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.

VERDICT.md verdict full narrative: mechanism, path:line, harness proof, fix
↓ download raw

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 clamps ndrives exactly like ciss.c:1357 (plus frees cll and returns); hunk 2 rejects out-of-range bus/target entries.
  • fix_apply.log: clean dry-run; fix_build.log: ciss.ko rebuilt with all 7 ciss pass-2 fixes at -Werror, link rc=0.
  • Harness -DFIXED run: 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Two 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
↓ fix.diffper-fix-DF-3729

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.