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

ciss_notify_complete: fatal-error path leaves ciss_periodic_notify dangling -> double-ownership, double post, xpt_done twice (CAM UAF)

Summary

The fatal-status exit of ciss_notify_complete calls ciss_release_request(cr) WITHOUT clearing sc->ciss_periodic_notify (the two sibling exits clear it). The next 10s ciss_periodic tick restarts the chain via the stale pointer: the request, by then re-allocated from the free list to a CAM I/O, is preened, CDB-clobbered and POSTED a second time; both completions run the CAM callback -> xpt_done(ccb) twice for one ccb and the request is double-released onto ciss_free (STAILQ corruption). VERIFIED: state-machine replica of the free-list/notify ownership - the CAM-owned request re-posted (posts=3), CAM callback running 3x for ONE ccb; -DFIXED replica clean. Adapter returns a fatal status (PROTOCOL_ERROR/HARDWARE_ERROR/CONNECTION_LOST) for the Notify-on-Event command - a real distressed-firmware condition; 10s later the driver corrupts its request pool and double-completes an in-flight CAM CCB. One-line fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3728 Β· 16 files
FileTypeDescriptionSize
harness.c trigger-source faithful userspace replica of the cited kernel path (real macros/struct constants) 7.5 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] 1.4 KB view raw
run.2.log run-log repeat run (stability) 1.4 KB view raw
run.3.log run-log repeat run (stability) 1.4 KB view raw
fix_run.log run-log -DFIXED section (persist_fix reads this) 481 B view raw
VERDICT.md verdict full narrative: mechanism, path:line, harness proof, fix 4.1 KB ↓ raw
fix.diff suggested-fix clear sc->ciss_periodic_notify before ciss_release_request in the notify fatal-error path 621 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.5 KB ↓ raw
manifest.json manifest this file 3.0 KB view raw
verdict.json verdict machine verdict (persist_poc.py schema) 2.5 KB view raw
README.md readme human reproduce doc
↓ download raw

DF-3728 β€” ciss_notify_complete fatal-error path leaves ciss_periodic_notify dangling β†’ double-ownership / double completion of a ciss_request

File: sys/dev/raid/ciss/ciss.c:3619-3627 (fatal path releases the request but does NOT clear sc->ciss_periodic_notify; the two sibling exit paths DO clear it: ciss.c:3648 and ciss.c:3653).

Class: use-after-free-style request lifetime bug (double ownership β†’ double post β†’ double completion β†’ xpt_done(ccb) twice β†’ CAM UAF), CWE-672/DF-3672-family "CCB lifetime" lesson.

Reproduce

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

Expected output

Baseline: step4 shows the CAM-owned request re-posted by the notify chain (posts=3), step5 shows the CAM callback running 3Γ— for ONE ccb, and [BUG REPRODUCED]. Fixed replica (-DFIXED = clear the pointer in the fatal path): periodic_notify = NULL (cleared), no reuse.

Trigger (kernel)

Adapter returns a fatal status (β‰  SUCCESS/TARGET_STATUS/TIMEOUT β€” e.g. CISS_CMD_STATUS_PROTOCOL_ERROR, HARDWARE_ERROR) for the Notify-on-Event command; 10 s later ciss_periodic (ciss.c:3427-3430) restarts the chain through the stale pointer.

Fix

fix.diff (clear ciss_periodic_notify before releasing). Compile-validated with the other 6 ciss fixes at -Werror (fix_build.log); behaviorally validated by the -DFIXED harness 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-3728 VERDICT

Finding

ciss_notify_complete() (sys/dev/raid/ciss/ciss.c:3598-3663) has three exit paths for the completed notify request:

  1. fatal error (ciss.c:3619-3627): ciss_release_request(cr); sc->ciss_flags &= ~CISS_FLAG_NOTIFY_OK; return; β€” sc->ciss_periodic_notify is NOT cleared β†’ dangling pointer to a request that is now on the free queue.
  2. notifier-aborted (ciss.c:3642-3649): releases AND clears (3648).
  3. event queued (ciss.c:3650-3656): clears (3653).

The consumer, ciss_notify_event() (ciss.c:3517-3595), starts with cr = sc->ciss_periodic_notify; (ciss.c:3526) and only allocates a fresh request if (cr == NULL) (ciss.c:3529). After a fatal notify status, CISS_FLAG_NOTIFY_OK is clear, so the next ciss_periodic() tick (ciss.c:3427-3430, ≀10 s later) calls ciss_notify_event(), which follows the stale pointer β€” a request that has been back on ciss_free and can have been handed to any other submitter (ciss_get_request, ciss.c:2409).

Consequence chain (all steps mirrored in the harness)

  1. ciss_get_request re-initializes the slot (ciss.c:2412-2416) and a CAM I/O owns it: cr_data = csio->data_ptr, cr_complete = ciss_cam_complete (ciss.c:3078-3081), posted to the adapter.
  2. The stale notify path reuses it: cr->cr_data != NULL so the 512-byte buffer allocation is skipped (ciss.c:3544-3547 β€” reads the NEW owner's data pointer), ciss_preen_command (ciss.c:3550) rewrites the header / error-info pointer, the notify CDB overwrites the CAM CDB (ciss.c:3554+), and ciss_start (ciss.c:3579) posts the same tag a second time.
  3. The adapter completes each post; ciss_complete (ciss.c:2164-2204) processes both completions: the second one hits "completing non-busy request" (ciss.c:2169-2170) and then invokes the CAM callback again β†’ ciss_cam_complete β†’ xpt_done((union ccb *)csio) (ciss.c:3286) twice for one CCB β†’ CAM-side use-after-free, plus ciss_release_request twice β†’ the same request linked twice on ciss_free (STAILQ cycle/corruption).

Additionally the CAM data buffer keeps being DMA'd per the clobbered SG state β€” the in-flight command is corrupted mid-execution.

Reachability / threat model

Precondition: the adapter returns a fatal command status for the Notify-on-Event command (CISS_CMD_STATUS_PROTOCOL_ERROR, CISS_CMD_STATUS_HARDWARE_ERROR, CONNECTION_LOST, ABORT_FAILED, …). That is a real-world condition on distressed/flaky Smart Array firmware β€” precisely the situations that also trip the heartbeat watchdog. No unprivileged-local trigger: this is a firmware-response-driven defect (same trust boundary as DF-1189..DF-1192).

Verification (no HP Smart Array on the audit guest)

harness.c models the request pool, free queue, notify state machine and completion dispatch 1:1 (kernel line cites inline). Baseline run (run.log, stable across run.2/run.3): - step4: tag1 now posts=3 (DOUBLE POST) with the CAM buffer retained; - step5: CAM callback (xpt_done) ran 3 time(s) for ONE ccb; - [BUG REPRODUCED]. -DFIXED replica (adds sc->ciss_periodic_notify = NULL; to the fatal path): periodic_notify = NULL (cleared) β€” the chain restarts on a fresh request, CAM request untouched.

Fix validation

  • fix.diff: one statement (sc->ciss_periodic_notify = NULL;) before ciss_release_request(cr) in the fatal path.
  • fix_apply.log: clean dry-run on guest /usr/src.
  • fix_build.log: ciss.ko rebuilt with all 7 ciss fixes at -Werror, link rc=0.
  • Harness -DFIXED run: dangling reuse GONE.

Bottom line: reproduced (harness-level, firmware-gated path); request lifetime defect leading to double completion / CAM CCB UAF; 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

sc->ciss_periodic_notify = NULL added before release in the fatal path. Module rebuilt at -Werror with all 7 fixes; -DFIXED replica clears the pointer and restarts on a fresh request.

fix.diff; fix_apply.log; fix_build.log; fix_run.log (periodic_notify = NULL (cleared))
↓ fix.diffper-fix-DF-3728

Confirmed kernel references

Detail

Exploit chain

fatal notify status -> dangling periodic_notify -> free-list reuse by CAM -> double post + command clobber -> double completion -> xpt_done x2+ / double free-queue insertion

Evidence (decisive lines)

run.log step4 posts=3 (DOUBLE POST), step5 CAM callback ran 3x for ONE ccb, [BUG REPRODUCED]; fix_run.log shows pointer cleared

PoC changes

harness written fresh; initial FIXED-path printf dereferenced the NULL pointer - fixed to print the cleared state

Verified recommended fix

clear sc->ciss_periodic_notify before ciss_release_request in the notify fatal-error path

Verdict

REPRODUCED (harness-level; firmware-gated path): the fatal-error exit of ciss_notify_complete (ciss.c:3619-3627) releases the notify request without clearing sc->ciss_periodic_notify (sibling exits clear it at 3648/3653). The next ciss_periodic tick restarts the chain via the stale pointer (ciss.c:3526): the request, by then re-allocated to a CAM I/O, is preened/CDB-clobbered and POSTED A SECOND TIME (ciss.c:3550,3579); both completions run the CAM callback -> xpt_done(ccb) multiple times for one ccb (ciss.c:3286) and double-release the request onto ciss_free -> STAILQ corruption / CAM UAF.