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

ciss_disable_adapter: uninitialized STAILQ head qh -> wild kernel write/read (missing STAILQ_INIT)

Summary

ciss_disable_adapter() declares cr_qhead_t qh without STAILQ_INIT. The first ciss_enqueue_complete(cr, &qh) expands to STAILQ_INSERT_TAIL (no NULL guard): *(qh.stqh_last) = cr writes the ciss_request kernel pointer through the uninitialized stack slot; the dequeue loop then dereferences the garbage qh.stqh_first. Reached from ciss_periodic after 3 failed 10s heartbeat checks with >=1 BUSY request. FreeBSD upstream HAS the init, making this a DragonFly-only divergence. VERIFIED: faithful queue.h-macro replica with stack grooming - '[WILD WRITE] *(0x51000000)=<request ptr>' 3/3 runs; gcc itself warns 'qh.stqh_last is used uninitialized'. Any system with an HP Smart Array whose adapter wedges (heartbeat watchdog trip) with queued I/O: uncontrolled write of a kernel heap pointer to a stack-garbage (groomable) address plus a wild read - panic to potential exploitation. One-line fix matching upstream.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3727 Β· 16 files
FileTypeDescriptionSize
harness.c trigger-source faithful userspace replica of the cited kernel path (real macros/struct constants) 6.0 KB view raw
build.sh build-script cc -O2 (and -DFIXED replica, -O0 variant) 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 496 B view raw
run.log run-log decisive run: baseline [BUG REPRODUCED] + fixed [FIXED] 2.9 KB view raw
run.2.log run-log repeat run (stability) 2.9 KB view raw
run.3.log run-log repeat run (stability) 2.9 KB view raw
fix_run.log run-log -DFIXED section (persist_fix reads this) 823 B view raw
VERDICT.md verdict full narrative: mechanism, path:line, harness proof, fix 4.1 KB ↓ raw
fix.diff suggested-fix add STAILQ_INIT(&qh) in ciss_disable_adapter (matches FreeBSD upstream) 415 B 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 1.9 KB ↓ raw
manifest.json manifest this file 2.9 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-3727 β€” ciss_disable_adapter: uninitialized STAILQ head β†’ wild kernel write

File: sys/dev/raid/ciss/ciss.c:3465 (cr_qhead_t qh; never STAILQ_INIT'd), used at ciss.c:3483 (ciss_enqueue_complete(cr, &qh) == STAILQ_INSERT_TAIL, sys/sys/queue.h:306 β€” no NULL guard: *(head)->stqh_last = (elm)).

Class: kernel memory corruption via uninitialized stack variable (CWE-457 β†’ CWE-787/CWE-824). DragonFly-only divergence: FreeBSD's ciss_disable_adapter has STAILQ_INIT(&qh); (upstream ciss.c:3581).

Trigger: adapter heartbeat unchanged for 3 consecutive 10 s checks (ciss_periodic, ciss.c:3389-3398) with β‰₯1 BUSY request β†’ ciss_disable_adapter (ciss.c:3395). The guest has no HP Smart Array hardware, so β€” per the established methodology (cf. DF-1190) β€” the primitive is proven with a faithful userspace harness replica using the exact queue.h macro text and stack grooming.

Reproduce

./build.sh && ./run.sh

Expected output

Baseline: [WILD WRITE] *(0x51000000) = <request ptr> and [BUG REPRODUCED] β€” STAILQ_INSERT_TAIL wrote the request pointer through the uninitialized qh.stqh_last (stack garbage groomed to 0x51000000). The -O0 build lands the groom deterministically (3/3 runs); gcc itself warns 'qh.stqh_last' is used uninitialized on the -O2 build (see build.log). Fixed replica (-DFIXED = STAILQ_INIT(&qh)): no write through garbage, list consistent, [FIXED] marker.

Fix

fix.diff (one line, matches FreeBSD upstream). Validated: applies cleanly, ciss.ko rebuilt with all 7 ciss fixes at -Werror (fix_build.log), and the -DFIXED harness run shows the wild write gone. No live-kernel trigger exists on the audit guest (no CISS controller).

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-3727 VERDICT

Finding

ciss_disable_adapter() (sys/dev/raid/ciss/ciss.c:3463-3507) declares

    cr_qhead_t          qh;              /* ciss.c:3465 */

and β€” unlike every other user of a local cr_qhead_t in this driver (ciss_intr ciss.c:2109, ciss_perf_msi_intr ciss.c:2136, ciss_cam_poll ciss.c:3194, ciss_poll_request ciss.c:2304) β€” never calls STAILQ_INIT(&qh). The first busy request is then enqueued:

    ciss_enqueue_complete(cr, &qh);      /* ciss.c:3483 */

which expands (sys/sys/queue.h:306-310) to

    (elm)->cr_link = NULL;
    *(head)->stqh_last = (elm);          /* WRITE through garbage pointer */

STAILQ_INSERT_TAIL in DragonFly has no NULL/stqh_last guard, so the very first store writes the kernel address of the ciss_request through whatever value the uninitialized stack slot holds. The subsequent ciss_dequeue_complete(sc, &qh) (ciss.c:3487) additionally dereferences the equally-uninitialized qh.stqh_first (STAILQ_REMOVE_HEAD reads STAILQ_NEXT(STAILQ_FIRST(head)) β†’ wild read).

Upstream divergence (proof this is a bug, not intent): FreeBSD's ciss_disable_adapter() contains STAILQ_INIT(&qh); right after the declaration block (FreeBSD src sys/dev/ciss/ciss.c:3581). DragonFly's copy (imported 2012 from FreeBSD 1.113 and diverging since) is missing it.

Reachability

ciss_disable_adapter is called from ciss_periodic() (ciss.c:3395) when the config-table heartbeat value is unchanged for 3 consecutive 10-second ticks (ciss.c:3389-3398) β€” i.e. the adapter wedged/locked up β€” and at least one request is BUSY (ciss.c:3477). On a system with real HP Smart Array hardware under I/O load this is the standard "failing adapter" path, not an exotic one: every hung controller with queued I/O that trips the 30 s heartbeat watchdog executes this function.

Primitive

  • Write: *(uint64_t*)GARBAGE = &sc->ciss_request[i] β€” an uncontrolled-but-groomable kernel pointer value written to a stack-garbage-controlled kernel address. The stack slot in ciss_disable_adapter's frame inherits whatever the prior user of that callout-thread stack depth left there (callout callbacks, earlier syscalls on the softclock thread); the harness demonstrates exactly this grooming.
  • Read: list traversal through garbage stqh_first on dequeue.

Impact ceiling: kernel memory corruption β†’ panic to potential exploitation depending on the groomed value (the write VALUE is a kernel heap pointer, limiting but not eliminating exploitation; the wild READ is a near-certain panic).

Verification (no HP Smart Array on the audit guest)

Faithful userspace replica (harness.c) with the exact queue.h macro text: - build.log: gcc 8.3 emits 'qh.stqh_last' is used uninitialized in this function [-Wuninitialized] on the -O2 build β€” the compiler independently confirms the defect. - run.log (also run.2/run.3, 3/3 stable): the -O0 build lands the groom deterministically; output shows [WILD WRITE] *(0x51000000) = 0x4023c0 (request pointer written at the groomed address) and [BUG REPRODUCED]. - -DFIXED replica (adds STAILQ_INIT(&qh)): no write through garbage, list head consistent, [FIXED].

Fix validation

  • fix.diff: one-line addition of STAILQ_INIT(&qh); β€” identical to FreeBSD upstream.
  • fix_apply.log: patch --dry-run succeeds on the guest's clean /usr/src copy.
  • fix_build.log: with all 7 ciss pass-2 fixes applied, ciss.ko rebuilt at -Werror, link rc=0.
  • Harness -DFIXED run in run.log: wild write GONE (baseline reproduces, patched replica does not).

Bottom line: reproduced (harness-level, hardware-gated path), kernel wild-write primitive via uninitialized stack variable; fix is the missing one-line STAILQ_INIT, 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

STAILQ_INIT(&qh) added (matches FreeBSD upstream ciss.c:3581). Module rebuilt with all 7 ciss fixes at -Werror (link rc=0); -DFIXED harness replica shows no write through garbage and consistent list head.

fix.diff; fix_apply.log (clean dry-run); fix_build.log (-Werror, ciss.ko link ok); fix_run.log ([FIXED] marker)
↓ fix.diffper-fix-DF-3727

Confirmed kernel references

Detail

Exploit chain

adapter wedge (heartbeat x3) -> ciss_disable_adapter -> *(garbage stqh_last)=&ciss_request[i] wild write -> panic..exploitation depending on stack grooming; wild read on dequeue

Evidence (decisive lines)

run.log lines with [WILD WRITE] *(0x51000000)=0x4023c0 and [BUG REPRODUCED]; build.log gcc -Wuninitialized; fix_run.log [FIXED]; VERDICT.md

PoC changes

harness written fresh (no seed); -O2 build shifts the frame so the deterministic -O0 build is the default demonstration

Verified recommended fix

add STAILQ_INIT(&qh) in ciss_disable_adapter (matches FreeBSD upstream)

Verdict

REPRODUCED (harness-level; hardware-gated path): ciss_disable_adapter (ciss.c:3465) never STAILQ_INITs its local cr_qhead_t qh; the first ciss_enqueue_complete (ciss.c:3483 == STAILQ_INSERT_TAIL, queue.h:306, no NULL guard) writes the ciss_request kernel pointer through the uninitialized stack slot qh.stqh_last, and the dequeue loop dereferences the garbage stqh_first. Reached from ciss_periodic after 3 failed 10s heartbeats with >=1 BUSY request (ciss.c:3389-3398,3395). FreeBSD upstream has STAILQ_INIT(&qh) (upstream ciss.c:3581) - DF-only divergence. Harness: 3/3 runs land a request-pointer wild write at the stack-groomed address; gcc itself warns -Wuninitialized (build.log).