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

sgopen: missing cam_periph_acquire plus cam_periph_unlock-without-lock in securelevel path (lock/refcount protocol violation; refcount underflow->UAF)

Summary

sgopen() deviates from every other CAM passthrough driver (compare scsi_pass.c:308-367) by NEVER calling cam_periph_acquire() to match sgclose() cam_periph_release() at scsi_sg.c:437. Worse in securelevel>1 error path it calls cam_periph_unlock cam_periph_release BEFORE any cam_periph_lock has been taken and BEFORE any acquire. Unlock-without-lock is lockmgr LK_RELEASE on unheld lock. Release-without-acquire is periph->refcount decrement with no matching increment. Because sgclose() also unconditionally calls cam_periph_release() every open/close pair nets one decrement of refcount. When periph marked CAM_PERIPH_INVALID (device removal) cam_periph_release frees it but cdev_t si_drv1 still points at freed periph so subsequent open/ioctl is UAF.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2282 Β· 6 files
FileTypeDescriptionSize
VERDICT.md verdict gate analysis + source trace + exploit-chain stop reason 2.8 KB ↓ raw
fix.diff suggested-fix mirror scsi_pass.c: add cam_periph_acquire+lock at top of sgopen, make securelevel/INVALID error paths symmetric 1.3 KB view raw
fix_build.log build-log nativekernel rc=0 with all fixes applied (-Werror -DINVARIANTS) 5.6 MB ↓ download
env.txt environment guest uname, kldstat, camcontrol devlist, /dev perms, pciconf, maxx groups 2.7 KB view raw
build.sh build-log documents the HW/permission gate (no PoC binary) 492 B view raw
run.sh run-log prints the gate proof 471 B view raw
VERDICT.md verdict gate analysis + source trace + exploit-chain stop reason
↓ download raw

DF-2282 β€” scsi_sg.c sgopen missing cam_periph_acquire (+ unlock-without-lock)

Verdict: NOT REPRODUCED (permission-gated) β€” source bug CONFIRMED real; fix.diff compiles.

Classification: not_reproduced / permission-gated for the unprivileged actor / impact=none. The refcount/lifetime bug is real in source; the defense-in-depth fix.diff compiles cleanly (nativekernel rc=0, -Werror, -DINVARIANTS).

Why not reproduced (the gate)

sgopen() is the open(2) handler of the sg(4) peripheral. /dev/sg0 on this guest is mode 0600 root:operator (root-only); maxx (uid 1001, in no privileged group) cannot open it, so the open/close refcount churn and the securelevel error path are unreachable by the unprivileged actor. A root trigger would be root→kernel (game-over by definition).

Gate proof (this guest):

$ ls -la /dev/sg0   ->  crw------- root operator /dev/sg0   (0600)
$ id maxx           ->  uid=1001(maxx) ... groups=1001(maxx)   [not operator]

The source bug (real, cited path:line)

sys/bus/cam/scsi/scsi_sg.c, sgopen() (:375-418): - Unlike every other CAM passthrough driver (compare scsi_pass.c:319 cam_periph_acquire + :322 cam_periph_lock), sgopen never calls cam_periph_acquire. sgclose() (:421-440) unconditionally calls cam_periph_release (:437). So every open/close pair nets one decrement of periph->refcount with no matching increment. - The securelevel > 1 error path (:395-398) calls cam_periph_unlock and cam_periph_release before any cam_periph_lock/acquire β€” unlock-without-lock (lockmgr LK_RELEASE on an unheld lock) and release-without-acquire (refcount decrement with no matching increment). - When the periph is later marked CAM_PERIPH_INVALID (device removal), cam_periph_release frees it, but cdev_t si_drv1 still points at the freed periph β†’ subsequent open/ioctl is a use-after-free.

Attacker model (per finding): the operator group on a host where /dev/sgN is group-accessible. On this guest the node is 0600 root-only, closing the unprivileged path.

Exploit chain

Not developed β€” /dev/sg0 is 0600; maxx cannot exercise the open/close churn. Valid hard blocker for the unprivileged model: the device node is not openable by the actor. (The UAF would additionally require device removal timing, but that is moot without an open path.)

Fix (defense-in-depth, validated to compile)

fix.diff mirrors scsi_pass.c: adds cam_periph_acquire + cam_periph_lock at the top of sgopen, makes the securelevel/INVALID error paths symmetric (unlock+release), and removes the bogus "else release" workaround. Validated in the combined nativekernel build (fix_build.log).

Reproduce

No runnable PoC for the unprivileged actor. Artifacts: env.txt, fix.diff, fix_build.log.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: PoC cannot run on this guest (HW/permission gated). fix.diff validated to APPLY cleanly and COMPILE in full nativekernel build (rc=0, -Werror, -DINVARIANTS) alongside sibling fixes (findings/poc/DF-2282/fix_build.log).

Compile validation: nativekernel build of patched tree -> 'NK_DONE rc=0'. No runtime test possible (HW/permission gated).
↓ fix.diffn/a (no target HW/device on this guest)

Confirmed kernel references

Detail

Exploit chain

none β€” valid hard blocker (driver code path dead / device node root-only / no malicious device present). No unprivileged->root path.

Evidence (decisive lines)

kldstat -> kernel, ehci.ko, xhci.ko (no target driver module); pciconf -l -> no SCSI/HBA HW; camcontrol devlist -> only <QEMU QEMU DVD-ROM> (sg0,pass0,cd0) on PIIX3 ata-cam; id maxx -> uid=1001(maxx) gid=1001(maxx) groups=1001(maxx) [not operator]. Source confirmed at cited lines.

PoC changes

Created findings/poc/DF-2282/{VERDICT.md, fix.diff, manifest.json, build.sh, run.sh, env.txt, fix_build.log}. No PoC source (HW/permission gated).

Verified recommended fix

fix.diff mirrors scsi_pass.c: add cam_periph_acquire+lock at sgopen, symmetric error-path release. Full git-apply-able diff in findings/poc/DF-2282/fix.diff.

Verdict

NOT REPRODUCED β€” HW/permission gated on this guest. The bug is REAL in source (traced line-by-line). scsi_sg.c sgopen missing cam_periph_acquire (refcount/lifetime UAF); /dev/sg0 0600 root-only, maxx EACCES. Gate confirmed via kldstat (only kernel+ehci+xhci), pciconf -l (no SCSI/HBA HW), camcontrol devlist (only QEMU DVD-ROM on PIIX3 ata-cam), and id maxx (uid 1001 not in operator) for the device-node findings.