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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4237 Β· 11 files
FileTypeDescriptionSize
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_IDENT without revoking in-flight ccbs β€” hot-plug glitch, camcontrol reset). Next XPT_SCSI_IO runs the probe (sili_cam.c:944-945); the renegotiation loop permanently holds every free slot with tag >= 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 call sili_cam_probe with 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); reporting qdepth=1 makes at_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;
  • guard mode (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-path sili_ata_get_xfer calls.

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.

VERDICT.md
↓ download raw

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:

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 at NULL->complete.
  • sili_cam.c:371-372 (NCQ renegotiation loop): xa = sili_ata_get_xfer(ap, atx); if (xa->tag < at->at_ncqdepth) β†’ NULL read at NULL->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_ccb and never placed on the free list; the other sc_ncmds-1 slots are. Free list capacity = 30.
  • sili_cam.c:101-115 β€” the CAM devq opening is sc_ncmds - 1 = 30 and max_tags starts at 1, raised to at_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's sili_ata_get_xfer returns NULL and xa->tag at :372 faults. With at_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_INQ wildcard (sili_cam.c:878-881), XPT_ENG_EXEC (sili_cam.c:846-850) and the port thread state machine (sili.c:511-512) run sili_port_state_machine β†’ sili_cam_probe with 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 sets ap_probe = ATA_PROBE_NEED_IDENT while in-flight ccbs stay ap_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.
  • guard mode (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 -l shows only i440FX/virtio; env.txt). The panic therefore cannot be triggered live on this guest β†’ not_reproduced, impact none (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:

  1. sili_cam.c:289 β€” on NULL, log + return (EBUSY) before the err: label (keeps at_probe = NEED_IDENT so the next XPT_SCSI_IO retries; at attach time the pool is full, so no behavior change at boot).
  2. 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).
  3. 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_testable
baseline no→ patch + rebuild →patched clean

not_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)
↓ fix.diffper-fix-DF-4237

Confirmed kernel references

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.