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

NULL-periph deref panic: targwrite()/targread() before TARGIOCENABLE call cam_periph_lock(softc->periph) with periph==NULL

Summary

Between open and a successful TARGIOCENABLE softc->periph is NULL; targwrite/targread deref it via cam_periph_lock with no state check - one 8-byte write(2) on a freshly opened /dev/targ panics deterministically. Guest-verified: Fatal trap 12 VA 0x40 'Stopped at targwrite+0x130'. Root-gated device (0600 + SYSCAP_RESTRICTEDROOT verified). ENXIO-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5374 Β· 13 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
poc5374.c β€” 2.1 KB view raw
build.sh β€” 156 B view raw
run.sh β€” 152 B view raw
run.log β€” 739 B view raw
run.patched.log β€” 743 B view raw
panic.txt β€” 781 B view raw
env.txt β€” 277 B view raw
fix.diff β€” 964 B view raw
fix_build.log β€” 494 B view raw
manifest.json β€” 957 B view raw
verdict.json β€” 3.3 KB view raw

DF-5374 β€” NULL-periph deref panic in targread()/targwrite() before LUN enable

What

sys/bus/cam/scsi/scsi_target.c

  • targwrite() (scsi_target.c:566-602) fetches softc and immediately uses softc->periph in cam_periph_lock() at :588/:602.
  • targread() (scsi_target.c:874, :880) does the same, plus dereferences softc->periph->sim->lock directly.
  • Neither path checks softc->periph != NULL or softc->state & TARG_STATE_LUN_ENABLED β€” between open(2) and a successful TARGIOCENABLE the periph pointer is NULL (set only in targctor(), scsi_target.c:522).
  • cam_periph_lock(periph) is CAM_SIM_LOCK(periph->sim) (cam_periph.h:186-190) β†’ NULL->sim read β†’ "Fatal trap 12" panic.

Threat

Any process that can open the targ clone node (created 0600 root:wheel by targinit(), scsi_target.c:1057-1063; open additionally gated by caps_priv_check_self(SYSCAP_RESTRICTEDROOT), :177). On the stock guest that means root/operator-with-devfs-rule; the canonical caller is the userland SCSI target daemon. One write(2) of 8 bytes before any setup panics the kernel β€” a trivially reliable local DoS with zero setup and no race.

Reproduce (guest, root)

cd /root/targpwn
cc -O0 -g -o poc5374 poc5374.c
./poc5374 w        # panic in targwrite/cam_periph_lock
# (after reset) ./poc5374 r   # panic in targread/cam_periph_lock

Expected

Kernel panic "Fatal trap 12: page fault while in kernel mode", faulting address = offsetof(struct cam_periph, sim), backtrace cam_periph_lock <- targwrite <- devfs_dwrite <- .... Serial console (vm.sh log) carries the panic; guest stops answering ssh (DDB).

Files

  • poc5374.c β€” trigger (write- and read-mode)
  • build.sh / run.sh
  • build.log / run.log / panic.txt β€” captured evidence
  • fix.diff β€” state-check both entry points, return ENXIO
VERDICT.md
↓ download raw

DF-5374 VERDICT

Status: REPRODUCED (panic / local DoS), fix validated

Root cause

targwrite() and targread() dereference softc->periph without any state check. softc->periph is set only by targctor() (scsi_target.c:522), i.e. by a successful TARGIOCENABLE; targopen() (scsi_target.c:168-204) leaves it NULL. Every pre-enable read(2)/write(2) executes cam_periph_lock(NULL) = CAM_SIM_LOCK(periph->sim) (cam_periph.h:186-190) β†’ NULL+0x40 read.

Guest evidence (baseline, kernel option targ, X86_64_GENERIC #0 Sat Oct 3 17:18:54)

  • write() path, ./poc5374 w (root):
  • serial console: Fatal user address access from kernel mode from poc5374 at ffffffff802c9930 / Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x40 / Stopped at targwrite+0x130: movq 0x40(%rax),%rax (panic.txt). Fault VA 0x40 = offsetof(struct cam_periph, sim); rax = NULL periph.
  • Guest stops in DDB; ssh dead. Deterministic, no race, no setup beyond opening the device.
  • read() path (targread, scsi_target.c:874): identical deref shape; not re-run live after the write-mode panic proved the primitive (same NULL periph, same lock call, cited by source).
  • Unprivileged user (maxx): open: Permission denied β€” the clone node is created 0600 root:wheel (make_autoclone_dev, scsi_target.c:1060) and targopen() enforces SYSCAP_RESTRICTEDROOT, so the stock-guest attacker is the (possibly compromised/buggy) privileged target daemon or any operator granted the device via devfs rules.

Impact

Deterministic local kernel panic (DoS) from one 8-byte write(2) on a freshly opened /dev/targ β€” before any LUN is enabled. No race window.

Exploitability beyond DoS

None for this primitive itself (NULL deref only), but it is a zero-cost reliable crash primitive for the daemon attack surface.

Fix validation

fix.diff adds the missing state checks (ENXIO when not enabled). Patched kernel (#2 Sat Oct 3 18:13:09 and #3): - ./poc5374 w β†’ write returned -1 (errno=6 ENXIO) - NO PANIC - ./poc5374 r β†’ read returned (errno=6) - NO PANIC - guest alive. Baseline panic gone. FIXED.

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

Baseline kernel: write()/read() before enable -> Fatal trap 12 in targwrite+0x130 (fault va 0x40), guest dies. Patched kernel (fix.diff state checks): write returned -1 errno=6 (ENXIO), read returned errno=6, no panic, guest healthy. Bad behavior gone.

run.patched.log; panic.txt (baseline).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Sat Oct 3 18:32:37 UTC 2026 x86_64

Confirmed kernel references

Detail

Exploit chain

open("/dev/targ", O_RDWR) -> write(fd, &ptr, 8) with ptr to any user CCB -> targwrite -> cam_periph_lock(NULL) -> read at NULL+0x40 -> Fatal trap 12 -> kernel panic (local DoS; no escalation potential from this primitive itself).

Evidence (decisive lines)

panic.txt (Fatal trap 12, fault va 0x40, Stopped at targwrite+0x130 movq 0x40(%rax),%rax); run.log (write-mode + unpriv EACCES); env.txt (uname, /dev/targ 0600 root:wheel); run.patched.log (write/read return ENXIO=6, guest alive).

PoC changes

No seed existed (pass-2 finding). PoC is minimal: open + 8-byte write of a pointer to a zeroed union ccb with func_code=XPT_NOOP; read-mode variant included.

Verified recommended fix

Reject I/O on unenabled softc: in targwrite()/targread() return ENXIO when softc->periph == NULL or !(softc->state & TARG_STATE_LUN_ENABLED).

Verdict

REPRODUCED: deterministic NULL-deref kernel panic from one 8-byte write(2) (or one read(2)) on /dev/targ before any TARGIOCENABLE. targwrite (sys/bus/cam/scsi/scsi_target.c:566-602) and targread (:874) call cam_periph_lock(softc->periph) with softc->periph == NULL (only targctor :522 sets it); cam_periph_lock is CAM_SIM_LOCK(periph->sim) (cam_periph.h:186-190). Guest proof: Fatal trap 12, fault virtual address 0x40 (= offsetof(struct cam_periph, sim)), 'Stopped at targwrite+0x130: movq 0x40(%rax),%rax', guest dead in DDB. Unprivileged users cannot reach it on the stock guest (clone node 0600 root:wheel + SYSCAP_RESTRICTEDROOT; verified live: EACCES/EPERM for non-root), so the attacker is the privileged/compromised target daemon or an operator granted the node via devfs rules. Fix validated on a patched kernel: both write() and read() now return ENXIO with no panic.