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)
PoC verification
Evidence pack
findings/poc/DF-4027 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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*:
- The polled command times out (sili.c:1346
while (timeout > 0)). timeout_fn(ccb)=sili_ata_cmd_timeoutβsili_core_timeout(ccb, 0)(sili.c:2440-2443).sili_core_timeoutmarks 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 staysATA_S_ONCHIP, still owned by the port (ap_activebit set, callout state live).sili_pollthen falls out and returnsccb->ccb_xa.stateβ stillATA_S_ONCHIP(sili.c:1359).- sili_cam.c releases the xfer anyway:
sili_ata_put_xfer(xa)βsili_put_ccb()(sili.c:2020-2030) pushes the port-owned ccb ontoap_ccb_freeand bumpsserial. (TheKKASSERT(ccb->ccb_xa.state == ATA_S_PUT)insili_get_ccbpasses 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_commandsORs the slot intoap_activea second time and incrementsap_active_cntagain (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,
completeissili_ata_complete_disk_rw(sili_cam.c:1331, 1638-β¦), which runsxpt_done()onxa->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)insili_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_testablenot_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
Confirmed kernel references
- sys/dev/disk/sili/sili.c:1311-1360
- sys/dev/disk/sili/sili.c:2440-2443
- sys/dev/disk/sili/sili.c:2488-2511
- sys/dev/disk/sili/sili.c:2002-2030
- sys/dev/disk/sili/sili.c:403-413
- sys/dev/disk/sili/sili.c:2392-2419
- sys/dev/disk/sili/sili.c:1532-1536
- sys/dev/disk/sili/sili.c:1113
- sys/dev/disk/sili/sili_cam.c:315-321
- sys/dev/disk/sili/sili_cam.c:543-595
- sys/dev/disk/sili/sili_cam.c:661-668
- sys/dev/disk/sili/sili_cam.c:1331-1334
- sys/dev/disk/sili/sili_cam.c:1638
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.
No comments yet.