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)
PoC verification
Evidence pack
findings/poc/DF-5374 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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) fetchessoftcand immediately usessoftc->periphincam_periph_lock()at :588/:602.targread()(scsi_target.c:874, :880) does the same, plus dereferencessoftc->periph->sim->lockdirectly.- Neither path checks
softc->periph != NULLorsoftc->state & TARG_STATE_LUN_ENABLEDβ between open(2) and a successful TARGIOCENABLE the periph pointer is NULL (set only intargctor(), scsi_target.c:522). cam_periph_lock(periph)isCAM_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
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
fixedBaseline 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).
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.
No comments yet.