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

sili deferred timeout hands a port-owned ccb (state ATA_S_ONCHIP) back to poll callers which sili_ata_put_xfer() it β€” ccb/slot double lifecycle: reissue on an active slot, stale sili_unload_prb of the new command's live dmamap, and double CAM completion

Summary

sili_poll returns xa->state unchanged after timeout_fn; when sili_core_timeout defers (other commands active), that value is the NON-terminal ATA_S_ONCHIP. Every poll caller in sili_cam.c treats != ATA_S_COMPLETE as terminal and calls sili_ata_put_xfer(), pushing a port-owned ccb onto ap_ccb_free. The recycled ccb is reissued on the same slot (double ap_active accounting); when the port drains, reinit's expired loop completes the stale request: unloads the NEW command's active dmamap (DMA writeback onto reused memory) then xpt_done()s the new request's CAM ccb - double CAM completion/UAF. INVARIANTS kernels KKASSERT in hardstop instead. Crafted-SATA-disk threat model (DF-3690..3696 family); model-proven interleaving (sili_model.c); HW-gated live. Guarded-release fix (not built - no sili hardware).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4027 Β· 9 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
sili_model.c β€” 3.3 KB view raw
build.sh β€” 167 B view raw
run.sh β€” 93 B view raw
build.log β€” 50 B view raw
run.log β€” 1.4 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.6 KB view raw

DF-4027 β€” sili deferred-timeout ccb ownership leak

File: sys/dev/disk/sili/sili.c (sink in sili_cam.c) β€” pass 2 (GLM 5.3). Code-trace verified; NOT live-reproducible: the audit guest has no sili(4) HBA (device-absent, not a false positive). See VERDICT.md for the complete line-verified trace.

THE BUG sili_poll() (sili.c:1311) can return the NON-terminal state ATA_S_ONCHIP: when the polled command times out while other commands are still active on the port, sili_core_timeout() defers (ap_expired bit set, return 1; sili.c:2505-2511) and sili_poll returns xa->state == ATA_S_ONCHIP (sili.c:1359).

Every poll caller in sili_cam.c treats != ATA_S_COMPLETE as
terminal and releases the xfer β€” sili_ata_put_xfer() at
sili_cam.c:318, 548, 570, 595, 668 β€” pushing a ccb the port still
owns (ap_active bit, expiration pending) back onto ap_ccb_free.

CONSEQUENCES * ccb/slot re-issued while on chip; ap_active_cnt double-counted (KKASSERT(ap_active_cnt == 0) panic in sili_port_hardstop on INVARIANTS kernels; counter corruption otherwise); * the stale completion in sili_port_reinit (sili.c:403-413) runs sili_unload_prb() on whatever dmamap the REISSUED command loaded (DMA writeback onto reused memory β€” the DF-4014 shape), and * calls ccb_xa.complete() β€” for CAM commands sili_ata_complete_disk_rw (sili_cam.c:1638) xpt_done()s the new request's CAM ccb: double CAM completion / CAM UAF.

TRIGGER Crafted SATA disk / port-multiplier target that stalls a polled probe/feature command (1000 ms) while another command is active on the same port. Same crafted-disk threat model as DF-3690..3696.

FILES VERDICT.md full trace, consequences, fix sili_model.c userspace model of the interleaving (no HW needed) build.sh/run.sh manifest.json / verdict.json

VERDICT.md
↓ download raw

DF-4027 VERDICT β€” sili deferred-timeout leaks a port-owned ccb back to the free list

Status: NOT REPRODUCED (hardware absent). The audit guest has no SiLion Image 3124/3132 HBA (sili(4) never attaches), so the trigger cannot be exercised live. The finding is code-trace proven: every step below is line-verified in this tree; the only unverifiable element is the device-side timing (a target that stalls a polled command past its timeout while another command is active on the same port β€” standard behavior for a broken/malicious SATA device behind a port multiplier, exactly the crafted-disk threat model of the DF-3690..3696 family).

Root cause (line-verified)

sili_poll() documents its return contract as "Returns ATA_S_ state" (sili.c:1294-1305) and every caller in sili_cam.c treats anything other than ATA_S_COMPLETE as terminal and releases the xfer (sili_ata_put_xfer at sili_cam.c:318/548/570/595/668). But sili_poll() can return ATA_S_ONCHIP β€” a non-terminal state*:

  1. The polled command times out (sili.c:1346 while (timeout > 0)).
  2. timeout_fn(ccb) = sili_ata_cmd_timeout β†’ sili_core_timeout(ccb, 0) (sili.c:2440-2443).
  3. sili_core_timeout marks the slot expired and, if any other command is still active on the port, defers: ap->ap_expired |= 1 << slot; if (ap->ap_active != ap->ap_expired) return (1); (sili.c:2505-2511) β€” the ccb stays ATA_S_ONCHIP, still owned by the port (ap_active bit set, callout state live).
  4. sili_poll then falls out and returns ccb->ccb_xa.state β€” still ATA_S_ONCHIP (sili.c:1359).
  5. sili_cam.c releases the xfer anyway: sili_ata_put_xfer(xa) β†’ sili_put_ccb() (sili.c:2020-2030) pushes the port-owned ccb onto ap_ccb_free and bumps serial. (The KKASSERT(ccb->ccb_xa.state == ATA_S_PUT) in sili_get_ccb passes later because put set it.)

From this point the ccb has two owners.

Consequences (trace)

  • ccb reuse while on-chip: the next sili_ata_get_xfer() hands the same ccb/slot to a new command; sili_issue_pending_commands ORs the slot into ap_active a second time and increments ap_active_cnt again (sili.c:1532-1536) β€” accounting inflated; two logical commands share one hardware slot.
  • Stale completion unloads the wrong dmamap: when the port drains, sili_port_reinit's expired loop completes the stale request: ccb->ccb_xa.state = ATA_S_TIMEOUT; ccb->ccb_done(ccb); ccb->ccb_xa.complete(&ccb->ccb_xa); (sili.c:403-413). sili_ata_cmd_done β†’ sili_unload_prb (sili.c:2415, 1271-1291) unloads whatever dmamap is currently loaded β€” the new command's β€” while the new command may still be on chip: DMA writeback onto memory the driver no longer owns (the DF-4014 mps shape).
  • Double CAM completion: if the reissued ccb carries a CAM command, complete is sili_ata_complete_disk_rw (sili_cam.c:1331, 1638-…), which runs xpt_done() on xa->atascsi_private β€” the new request's CAM ccb β€” completing it out from under its own lifecycle: CAM UAF / double completion.
  • ap_active_cnt underflow / INVARIANTS panic: the double-counted slot is decremented twice; KKASSERT(ap->ap_active_cnt == 0) in sili_port_hardstop (sili.c:1113) fires on INVARIANTS kernels; on production kernels the counter wraps and the port state machine corrupts.

Trigger requirements (crafted-disk)

  • sili HBA + port multiplier (or any second active command on the port)
  • a target that answers a POLLED probe/feature command (IDENTIFY / SET_FEATURES / SETXFER, 1000 ms timeout, sili_cam.c:313/541/563/588/655) slower than the timeout while another target's command is active. Poll users: probe path (sili_cam.c:315-321, 543-548, 565-570, 590-595, 661-668) and CAM_POLLED disk I/O (sili_cam.c:1333-1334).

Why not DF-4014/2517/2518/1338/1339

  • DF-4014 (mps_user.c) frees an in-flight command itself on wait timeout; here the driver's poll/timeout state contract hands a still-owned ccb to callers that release it β€” different file, different mechanism (deferral, not free-on-timeout).
  • The sili knowns (SGE table bounds, NCQ tag OOB) are unrelated paths.

Fix

sili_poll() must not return ATA_S_ONCHIP/ATA_S_PENDING to callers whose contract is "state is terminal after return". Minimal fix: in sili_core_timeout, when the timeout is deferred and the caller is the poll path, the ccb must not be put back; better, sili_poll's callers should put the xfer only on terminal states. Concrete one-line-ish guard in sili_cam.c: replace the unconditional sili_ata_put_xfer(xa) on the != ATA_S_COMPLETE paths with a check if (xa->state == ATA_S_ONCHIP || xa->state == ATA_S_PENDING) { do NOT put; the port still owns the ccb; report failure and leave completion to the port }. (The CAM-polled path at sili_cam.c:1343-1354 already behaves correctly β€” it ignores the return and lets the port complete.)

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: guest has no sili HBA; proposed guard in VERDICT.md (do not sili_ata_put_xfer on non-terminal ATA_S_ONCHIP/ATA_S_PENDING poll returns; mirror the CAM-polled path which already leaves the ccb to the port)

VERDICT.md 'Fix' section
per-fix-DF-4027

Confirmed kernel references

Detail

Exploit chain

crafted SATA device / PM target stalls a polled probe command (IDENTIFY/SET_FEATURES, 1000ms) while another command is active on the same port -> deferred timeout returns ATA_S_ONCHIP -> sili_cam put_xfer of port-owned ccb -> ccb+slot reused by next command -> port drain completes stale request -> bus_dmamap_unload of the active new transfer (DMA writeback onto freed/reused memory) + double xpt_done on a live CAM ccb -> kernel memory corruption in the CAM/SILI boundary (or INVARIANTS panic via ap_active_cnt underflow at sili_port_hardstop).

Evidence (decisive lines)

VERDICT.md (full line-verified trace incl. every poll call site and the two completion sinks); sili_model.c prints the interleaving; no hardware on guest so no run.log - status reflects device absence, not refutation

PoC changes

No seed PoC existed; sili_model.c is a userspace model of the kernel interleaving (hardware trigger not available on the guest).

Verified recommended fix

In sili_cam.c poll error paths, only sili_ata_put_xfer() when xa->state is terminal (not ATA_S_ONCHIP/ATA_S_PENDING); the port retains ownership of deferred cbs and completes them via sili_port_reinit().

Verdict

Code-trace proven; not live-reproducible on the audit guest because no sili(4) HBA exists there (device-absent, not a false positive): sili_poll() returns the non-terminal ATA_S_ONCHIP to its caller whenever sili_core_timeout() defers the timeout (other commands still active on the port, sili.c:2505-2511 -> 1359), and every poll caller in sili_cam.c then does sili_ata_put_xfer() on the still port-owned ccb (sili_cam.c:318/548/570/595/668). The recycled ccb is re-issued on the same slot (double ap_active accounting), and the stale completion in sili_port_reinit (sili.c:403-413) unloads the NEW command's active dmamap and runs complete() - for CAM commands sili_ata_complete_disk_rw -> xpt_done on the new request's CAM ccb (double CAM completion / UAF), or trips KKASSERT(ap_active_cnt==0) on INVARIANTS kernels.