sili(4): sili_cam_probe() dereferences unchecked sili_ata_get_xfer() NULL β kernel panic when a port reprobe runs against a saturated ccb pool (IDENTIFY write at :290, NCQ-renegotiation read at :372)
Summary
sili_ata_get_xfer returns NULL when the port ccb free list is exhausted, but sili_cam.c dereferences the result unconditionally at the IDENTIFY write and the NCQ renegotiation read. The renegotiation loop permanently holds every free slot with tag >= at_ncqdepth, so a reprobe concurrent with a saturated CAM queue drains the pool and faults on NULL. A crafted SATA disk triggers deterministically (force error-recovery softreset with I/O in flight, then report identify qdepth=1 so only tag 0 cycles). Pool model run on the guest: shipped mode prints the NULL deref with 29 in-flight + qdepth=1; guard mode defers cleanly. HW-gated live. NULL-check fix (validated dry-run).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4237 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| sili_ncq_pool_model.c | β | 6.9 KB | view raw | |
| build.sh | β | 103 B | view raw | |
| run.sh | β | 114 B | view raw | |
| build.log | β | 13 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| env.txt | β | 366 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-4237 β sili(4): unchecked sili_ata_get_xfer() NULL dereference in probe / NCQ-renegotiation (kernel panic)
What
sili_cam_probe() (sys/dev/disk/sili/sili_cam.c) calls sili_ata_get_xfer()
at sili_cam.c:289 (IDENTIFY) and sili_cam.c:371 (NCQ-renegotiation loop) and
dereferences the returned struct ata_xfer * without a NULL check. The core
helper explicitly returns NULL when the port's ccb free list is exhausted
(sili.c:2318-2327, DPRINTF("... NULL ccb")), which happens whenever the CAM
command queue is saturated at reprobe time. Result: write/read through NULL
(xa->complete = ... at sili_cam.c:290; xa->tag read at sili_cam.c:372) β
kernel page fault β panic.
Threat model / reachability
- Physical disk, local user (race): heavy NCQ I/O (queue saturated, 30 of
31 slots held) concurrent with any event that re-probes the port
(link error recovery softreset β sili.c:845-849 sets
ap_probe = ATA_PROBE_NEED_IDENTwithout revoking in-flight ccbs β hot-plug glitch,camcontrol reset). NextXPT_SCSI_IOruns the probe (sili_cam.c:944-945); the renegotiation loop permanently holds every free slot withtag >= at_ncqdepth(sili_cam.c:370-376) and drains the pool β NULL deref.XPT_PATH_INQ(wildcard) /XPT_ENG_EXEC/ the port-thread state machine callsili_cam_probewith no devq credit (sili_cam.c:846-850, 878-881; sili.c:511-512), so with a saturated queue even the IDENTIFY itself NULL-derefs (sili_cam.c:289-290). - Crafted SATA disk (deterministic, same threat model as DF-3690../DF-4027):
the device feeds identify word 75 (
qdepth); reportingqdepth=1makesat_ncqdepth = 2(sili_cam.c:365) so only tag 0 cycles in the loop, making the pool drain near-certain after the device forces an error-recovery reprobe with I/O in flight.
Impact: NULL-pointer kernel panic (local DoS). Not a memory-corruption primitive (fault on the NULL direct-map entry, not attacker-mappable memory).
Evidence in this pack
sili_ncq_pool_model.cβ userspace model encoding the exact pool rules (31 slots, slot 1 reserved as err ccb, TAILQ head-pop/tail-push, devq opening 30, renegotiation loop semantics). Built and run on the audit guest with DragonFly cc 8.3:- shipped-kernel mode: Scenario A prints the panic site
(
sili_cam.c:372 xa->tag read through NULL xa after loop held 1 slots (iteration 2/31)), Scenario C (idle port) is the negative control; guardmode (models fix.diff): both scenarios defer, no panic.run.logβ full untrimmed guest output of both modes.VERDICT.mdβ full line-cited trace, honest classification.fix.diffβ NULL guards at the two reachable probe sites + defensive guards on the I/O-pathsili_ata_get_xfercalls.
Reproduce
# on the DragonFly guest (no sili HBA needed for the model) cc -O2 -Wall -o sili_model sili_ncq_pool_model.c ./sili_model # shipped-kernel semantics -> PANIC emulated at :372 ./sili_model guard # fix.diff semantics -> defers, no panic
Live kernel trigger requires a SiI 3124/3132 HBA with a disk (or crafted
SATA device); the audit guest has none (see env.txt), so the finding is
classified not_reproduced / hardware-gated, code-trace + model proven β
same discipline as DF-4027.
DF-4237 VERDICT β unchecked sili_ata_get_xfer() NULL dereference in sili_cam.c probe paths
Status: not_reproduced (hardware-gated: guest has no sili(4) HBA). Code-trace + guest-run model: proven. Confidence: likely. Impact if triggered: kernel NULL-pointer panic (local DoS).
1. Root cause (path:line)
sili_ata_get_xfer() returns NULL when the port ccb free list is empty:
- sys/dev/disk/sili/sili.c:2318-2327 β
ccb = sili_get_ccb(ap); if (ccb == NULL) { DPRINTF(... "NULL ccb"); return (NULL); } - sys/dev/disk/sili/sili.c:2003-2018 β
sili_get_ccb()popsap_ccb_free, returns NULL when empty. No blocking, no waiting.
sili_cam.c dereferences the result unconditionally at (at least) these sites:
- sili_cam.c:289-294 (IDENTIFY step of
sili_cam_probe):xa = sili_ata_get_xfer(ap, atx); xa->complete = sili_ata_dummy_done; xa->data = ...β NULL write atNULL->complete. - sili_cam.c:371-372 (NCQ renegotiation loop):
xa = sili_ata_get_xfer(ap, atx); if (xa->tag < at->at_ncqdepth)β NULL read atNULL->tag. - Defensive (same shape, currently shielded by devq accounting but exposed to
the same renegotiation race window): sili_cam.c:533, 556, 583, 652
(probe_disk/set_xfer), sili_cam.c:1026 (
sili_xpt_scsi_disk_io), sili_cam.c:1420 (sili_xpt_scsi_atapi_io).
2. Why the free list can be empty when the probe runs
Pool construction:
- sili.h:706 β
SILI_MAX_CMDS 31(sc_ncmds β€ 31). - sili.c:265-268 β slot 1 is reserved as
ap_err_ccband never placed on the free list; the othersc_ncmds-1slots are. Free list capacity = 30. - sili_cam.c:101-115 β the CAM devq opening is
sc_ncmds - 1= 30 andmax_tagsstarts at 1, raised toat_ncqdepth - 1(β€ 30) only after NCQ negotiation (sili_cam.c:377-380). So up to 30 CAM ccbs can concurrently hold an xa.
Probe entry points have unequal protection:
XPT_SCSI_IO(sili_cam.c:939-963): the current ccb occupies a devq slot but has not yet taken an xa, so β€ 29 others hold xas β β₯ 1 free slot for the IDENTIFY at :289. But the renegotiation loop at :370-376 permanently holds every free slot whose tag β₯at_ncqdepth("throw away any ata_xfer's beyond the negotiated number of slots"). With the last free slot held and no completion in the window, the next loop iteration'ssili_ata_get_xferreturns NULL andxa->tagat :372 faults. Withat_ncqdepth = 2(device identify word 75 reports qdepth=1, sili_cam.c:365) only tag 0 cycles, so the drain is near-certain.XPT_PATH_INQwildcard (sili_cam.c:878-881),XPT_ENG_EXEC(sili_cam.c:846-850) and the port thread state machine (sili.c:511-512) runsili_port_state_machineβsili_cam_probewith no devq credit; with 30 in-flight ccbs the free list is empty and even the IDENTIFY at :289-290 NULL-derefs.
Path to NEED_IDENT without revoking in-flight ccbs:
- sili.c:845-849 β
sili_port_softreset()success setsap_probe = ATA_PROBE_NEED_IDENTwhile in-flight ccbs stayap_active(they hold their slots until their own timeouts). - Softreset is reached from
XPT_RESET_DEV(sili_cam.c:903-913), from error recovery, and from the state machine (sili.c:509-510).
3. Model verification (run on the audit guest)
sili_ncq_pool_model.c encodes the pool rules exactly (31 slots, slot 1
reserved, TAILQ head-pop/tail-push per sili.c:2011/2028, devq opening 30,
renegotiation loop of sili_cam.c:370-376, at_ncqdepth = (qdepth & 0x1F)+1
of sili_cam.c:365-368). Guest run (DragonFly 6.5-DEVELOPMENT, cc 8.3):
- Scenario C (idle port, control): no NULL β the bug needs concurrent I/O.
- Scenario A (XPT_SCSI_IO reprobe, 29 in-flight, device re-reports
qdepth=1):
PANIC: sili_cam.c:372 xa->tag read through NULL xa after loop held 1 slots (iteration 2/31)β the loop holds the single free slot (tag 30 β₯ ncqdepth 2), the next iteration gets NULL, the kernel derefs. guardmode (fix.diff semantics): defers, no panic, both A and B.
See run.log (full untrimmed output) and build.log.
4. Honest classification
- The guest kernel contains sili(4) compiled in (
kldload siliβ "module already loaded or in kernel") but has no SiI 3124/3132 PCI device (pciconf -lshows only i440FX/virtio;env.txt). The panic therefore cannot be triggered live on this guest βnot_reproduced, impactnone(nothing demonstrated live), per the same discipline used for DF-4027, DF-2517, DF-2518. - On real hardware this is a local-DoS panic requiring queue saturation concurrent with a reprobe (error recovery / reset / hot-plug). A crafted SATA device (the audit's DF-3690.. family threat model) triggers it deterministically by reporting qdepth=1 after forcing an error-recovery softreset with I/O in flight.
- Not memory corruption: the faulting access is to the NULL direct-map page; no attacker-controlled content is written or read (CWE-476).
5. Fix
fix.diff in this directory:
- sili_cam.c:289 β on NULL, log +
return (EBUSY)before the err: label (keepsat_probe = NEED_IDENTso the next XPT_SCSI_IO retries; at attach time the pool is full, so no behavior change at boot). - sili_cam.c:371 β
if (xa == NULL) break;in the renegotiation loop (pool exhausted by in-flight I/O; the held slots still achieve the "reserve one slot for recovery" intent). - Defensive guards at sili_cam.c:1026/1420 completing the ccb with
CAM_RESRC_UNAVAIL.
fix_status: not_testable (no sili HBA on the guest; guards are modeled in
./sili_model guard instead).
6. Relation to known findings
Distinct from the sili knowns: DF-1338/DF-2517 (SGE/PRDT overflow in
sili_load_prb_callback), DF-1339/DF-2518 (NCQ error tag OOB),
DF-4027 (deferred-timeout ccb double lifecycle β that finding is about
put_xfer of port-owned ccbs; this one is about the absence of a NULL
check on get_xfer), DF-1462/DF-2519 (ATA_PASS_16 copy-paste).
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
not_testablenot_testable on the guest (no sili HBA to build a kernel for); fix.diff adds NULL guards at sili_cam.c:289 (return EBUSY keeping NEED_IDENT so the probe retries), :371 (break out of the renegotiation loop when the pool is drained), and defensive CAM_RESRC_UNAVAIL completions at :1026/:1420; guards are modeled by ./sili_model guard which defers both panic scenarios with exit 0.
run.log (guard mode) + fix.diff (patch --dry-run verified against the unmodified sys/dev/disk/sili/sili_cam.c)
Confirmed kernel references
- sys/dev/disk/sili/sili_cam.c:289-294
- sys/dev/disk/sili/sili_cam.c:365-381
- sys/dev/disk/sili/sili_cam.c:944-945
- sys/dev/disk/sili/sili_cam.c:1026
- sys/dev/disk/sili/sili_cam.c:1420
- sys/dev/disk/sili/sili.c:2003-2018
- sys/dev/disk/sili/sili.c:2318-2327
- sys/dev/disk/sili/sili.c:511-512
- sys/dev/disk/sili/sili.c:845-849
Detail
Exploit chain
saturated NCQ queue (30 CAM ccbs hold xas) + reprobe event (error-recovery softreset / camcontrol reset / hot-plug glitch sets NEED_IDENT) -> next XPT_SCSI_IO (or PATH_INQ/ENG_EXEC/port-thread) runs sili_cam_probe -> NCQ renegotiation loop holds the last free slot (device-fed qdepth=1 -> at_ncqdepth=2) -> sili_ata_get_xfer returns NULL -> xa->tag read through NULL at sili_cam.c:372 -> kernel page fault on NULL direct-map -> panic (local DoS; crafted-SATA-disk threat model triggers deterministically, same family as DF-3690../DF-4027).
Evidence (decisive lines)
run.log: guest-built model, shipped mode prints 'PANIC: sili_cam.c:372 xa->tag read through NULL xa after loop held 1 slots (iteration 2/31)' and exits 3; 'guard' mode (fix.diff semantics) defers with no panic; Scenario C idle-port control shows no NULL without concurrency. VERDICT.md has the full line-cited trace; env.txt documents the absent SiI hardware on the guest.
PoC changes
No seed PoC; sili_ncq_pool_model.c is a userspace model of the exact pool rules (31 slots, slot 1 err-reserved, TAILQ head-pop/tail-push, devq opening 30, renegotiation loop semantics), run on the guest in both shipped and guarded modes.
Verified recommended fix
NULL-check every sili_ata_get_xfer() result in sili_cam.c (defer the probe with EBUSY at :289, break the renegotiation loop at :371, complete I/O ccbs with CAM_RESRC_UNAVAIL at :1026/:1420).
Verdict
Code-trace proven; not live-triggerable on the audit guest because no sili(4) HBA exists there (device-absent, not a false positive): sili_ata_get_xfer() returns NULL when the ccb free list is empty (sili.c:2318-2327 -> 2003-2018), and sili_cam.c dereferences the result unconditionally - xa->complete write at sili_cam.c:290 (IDENTIFY, reached from XPT_PATH_INQ-wildcard / XPT_ENG_EXEC / port-thread probes which hold no devq credit, sili_cam.c:846-850,878-881, sili.c:511-512) and xa->tag read at sili_cam.c:372 (NCQ renegotiation loop, reached from the XPT_SCSI_IO reprobe path sili_cam.c:944-945). The loop permanently holds every free slot with tag >= at_ncqdepth (sili_cam.c:370-376) and drains the pool: with 29 in-flight ccbs and a device re-reporting qdepth=1 (at_ncqdepth=2, only tag 0 cycles), iteration 2 gets NULL and the kernel faults on the NULL direct-map page -> panic. sili_port_softreset success sets ap_probe=NEED_IDENT without revoking in-flight ccbs (sili.c:845-849), which is the natural entry to the race after error recovery / XPT_RESET_DEV.
No comments yet.