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

sgopen unlocks an unheld lock and releases a periph reference that was never acquired

Field Value
ID DF-1051
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-667 Improper Locking; CWE-911 Improper Update of Reference Count
File sys/bus/cam/scsi/scsi_sg.c
Lines 388-418 (sgopen, esp. 395-399 securelevel error path), 421-441 (sgclose)
Area bus/cam/scsi (CAM SCSI generic passthrough /dev/sgN)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

sgopen is missing the cam_periph_acquire() call that the sibling pt driver (scsi_pt.c) makes at the top of its open routine. In the securelevel > 1 error path, cam_periph_unlock(periph) is called at line 396 BEFORE cam_periph_lock(periph) is ever called (the lock is acquired at line 400, after the check). This releases a lockmgr lock that the current thread does not own. Additionally, every cam_periph_release() call in sgopen (lines 397, 414) and sgclose (line 437) drops a reference that was never acquired, causing the periph refcount (u_int32_t, initialised to 0 at cam_periph.c:215) to underflow to 0xFFFFFFFF.

Root cause

Compare sgopen (scsi_sg.c:374-418) with ptopen: ptopen calls cam_periph_acquire(periph) BEFORE locking and BEFORE any error-path release. sgopen skips the acquire entirely.

This means:

(a) At lines 395-399, the securelevel > 1 path calls cam_periph_unlock(periph) β€” but cam_periph_lock has NOT been called yet (it is called at line 400). cam_periph_unlock calls lockmgr(periph->sim->lock, LK_RELEASE) (cam_sim.c:64-70). Releasing a lockmgr lock you don't own is a lockmgr violation that panics the kernel under INVARIANTS and corrupts lock state in production builds.

(b) The cam_periph_release(periph) at line 397, 414, and sgclose line 437 all decrement periph->refcount (u_int32_t, cam_periph.h:123) which started at 0. Each release underflows it to 0xFFFFFFFF (first call), 0xFFFFFFFE (second call), etc. This permanently prevents the periph from being freed via cam_periph_release (cam_periph.c:341-378 checks refcount == 1 for the free path, which can never be reached after underflow). The periph and softc are leaked for the lifetime of the system.

/* scsi_sg.c:388-418 β€” the bug */
periph = (struct cam_periph *)ap->a_head.a_dev->si_drv1;
if (periph == NULL)
    return (ENXIO);
/* !!! no cam_periph_acquire(periph) here */

if (securelevel > 1) {
    cam_periph_unlock(periph);     /* unlock without prior lock */
    cam_periph_release(periph);    /* release without prior acquire β€” refcount underflow */
    return(EPERM);
}
cam_periph_lock(periph);
...
/* normal-path release at line 414 also has no matching acquire */

Threat model & preconditions

  • Attacker position: Local root (SYSCAP_RESTRICTEDROOT is required by sgopen line 385).
  • Privileges gained or impact:
  • Lock-state corruption panic (immediate): triggered by any root-level process calling open() on /dev/sg* when securelevel > 1. In hardened server configurations that set securelevel > 1, any administrative tool, backup agent, or monitoring daemon that probes SCSI devices triggers an immediate kernel panic (lockmgr assertion failure or lock state corruption). Reliable local DoS.
  • Refcount underflow + memory leak (latent, all configs): every open/close cycle underflows the periph refcount, and the corrupted refcount prevents proper device teardown on hot-unplug.
  • Required config or capabilities: Default kernel with sg configured. Root to trigger.
  • Reachability: open("/dev/sg0", O_RDWR) as root. The securelevel > 1 panic requires sysctl kern.securelevel=2; the refcount underflow happens on every open/close in every config.

Proof of concept

Trigger the panic (root + securelevel > 1)

#include <fcntl.h>
#include <unistd.h>
int main(void) {
    /* As root, first raise securelevel: sysctl kern.securelevel=2 */
    int fd = open("/dev/sg0", O_RDWR);
    /* Kernel panics here: lockmgr LK_RELEASE on unowned lock */
    if (fd >= 0) close(fd);
    return 0;
}

Trigger the refcount underflow (any securelevel)

Open and close /dev/sg0 repeatedly as root; each cycle calls cam_periph_release on a zero refcount, underflowing periph->refcount. Hot-unplugging the SCSI device after this fails to free the periph (memory leak, observable via vmstat -m M_CAMPERIPH).

Build & run

cc -o sg_panic sg_panic.c
sudo sysctl kern.securelevel=2
sudo ./sg_panic

Expected output

INVARIANTS kernel:

panic: lockmgr: LK_RELEASE on unowned lock
cpuid = 0
Trace:
lockmgr() at kern_lock.c:...
cam_periph_unlock() at cam_sim.c:65
sgopen() at scsi_sg.c:396
spec_open() at ...

Production kernel: silent lock state corruption; later lock operations on periph->sim->lock may deadlock or panic in unrelated code paths.

For the underflow (any kernel):

(open /dev/sg0; close /dev/sg0) x N as root
vmstat -m | grep CAMPERIPH    # shows monotonically growing allocation count

Impact

Local DoS via kernel panic (lockmgr LK_RELEASE on unowned lock) when securelevel > 1 and any root tool opens /dev/sg*. Persistent memory leak + broken hot-unplug cleanup on every open/close in any configuration. Requires root, hence Medium rather than High.

Add cam_periph_acquire() after fetching periph (mirroring scsi_pt.c), and remove the spurious unlock in the securelevel path:

--- a/sys/bus/cam/scsi/scsi_sg.c
+++ b/sys/bus/cam/scsi/scsi_sg.c
@@ -386,9 +386,12 @@ sgopen(struct dev_open_args *ap)
    periph = (struct cam_periph *)ap->a_head.a_dev->si_drv1;
    if (periph == NULL)
        return (ENXIO);
+   if (cam_periph_acquire(periph) != CAM_REQ_CMP)
+       return (ENXIO);

    /*
     * Don't allow access when we're running at a high securelevel.
     */
    if (securelevel > 1) {
-       cam_periph_unlock(periph);
        cam_periph_release(periph);
        return(EPERM);
    }

This matches ptopen's pattern: acquire first, then release in every error/exit path. The normal-path releases at line 414 and sgclose line 437 now correspond to the new acquire.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1051 Β· 13 files
FileTypeDescriptionSize
sg_securelevel_panic.c trigger-source open /dev/sg0 to drive sgopen securelevel path 2.1 KB view raw
build.sh build-script cc -o sg_securelevel_panic sg_securelevel_panic.c 141 B view raw
run.sh run-script run as root after sysctl kern.securelevel=2 394 B view raw
fix.diff suggested-fix add cam_periph_acquire + drop spurious unlock 512 B view raw
build.log build-log PoC build log 460 B view raw
run.log run-log decisive run + panic signature 1.1 KB view raw
panic.txt panic-signature lwkt_reltoken illegal release at sgopen+0x85 654 B view raw
env.txt environment guest uname, securelevel, cc version, /dev/sg0 560 B view raw
fix_build_full.log build-log combined 5-patch kernel build (rc=0) 5.6 MB ↓ download
fix_run.log run-log patched-kernel re-test: open returns EPERM, guest stays up 907 B view raw
VERDICT.md verdict detailed analysis 4.0 KB ↓ raw
README.md readme human-readable summary 1.2 KB ↓ raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 329 B view raw
README.md readme human-readable summary
↓ download raw

DF-1051 β€” sgopen unlocks unheld lock + releases unacquired periph

Summary

sgopen at sys/bus/cam/scsi/scsi_sg.c:395-399 calls cam_periph_unlock(periph) in the securelevel > 1 error path BEFORE cam_periph_lock() is acquired at line 400 β€” releasing a lockmgr lock the caller does not own β†’ lwkt_reltoken: illegal release panic.

Additionally sgopen is missing the cam_periph_acquire() that sibling ptopen (scsi_pt.c:154) makes; every cam_periph_release() in sgopen (:397, :414) and sgclose (:437) underflows the periph refcount from 0 β†’ 0xFFFFFFFF, breaking hot-unplug cleanup.

Trigger

Local root (SYSCAP_RESTRICTEDROOT):

sysctl kern.securelevel=2
cc -o sg_securelevel_panic sg_securelevel_panic.c
./sg_securelevel_panic

Unpatched kernel panics. Patched kernel returns EPERM cleanly.

Build / Run

./build.sh    # cc -o sg_securelevel_panic sg_securelevel_panic.c
sysctl kern.securelevel=2     # as root, before ./run.sh
./run.sh

Fix

fix.diff adds the missing cam_periph_acquire and removes the spurious unlock in the securelevel path, mirroring ptopen. Validated on a single-fix kernel build (#1, sha256 bcfe20d5…): no panic, open returns EPERM.

VERDICT.md verdict detailed analysis
↓ download raw

DF-1051 β€” sgopen unlocks unheld lock + releases unacquired periph (REPRODUCED)

Verdict

REPRODUCED β€” kernel panic observed on the unpatched #0 baseline kernel; fix validated on the patched #1 kernel.

Mechanism (confirmed by runtime panic)

sys/bus/cam/scsi/scsi_sg.c:sgopen (the /dev/sg* open path) has two distinct bugs:

(1) Unlocks a lock it never acquired. At :395-399, the securelevel > 1 error path calls cam_periph_unlock(periph) BEFORE cam_periph_lock() is called at :400. cam_periph_unlock calls lockmgr(periph->sim->lock, LK_RELEASE) which decrements the lwkt token reference count for periph->sim->lock β€” but no token was held. The token system panics with lwkt_reltoken: illegal release at sys/sys/lwktoken.c from inside sgopen+0x85. Trace captured in panic.txt:

REF CONTENT: tok=0 count=0000000000000000 owner=0xfffff8008e11d658
lwkt_reltoken: no tokens to release
panic: lwkt_reltoken: illegal release
cpuid = 1
Trace:
lwkt_reltoken() at lwkt_reltoken+0xda
lwkt_reltoken() at lwkt_reltoken+0xda
sgopen() at sgopen+0x85
dev_dopen() at dev_dopen+0x6c
devfs_spec_open() at devfs_spec_open+0x27d
vop_open() at vop_open+0x7c

(2) Releases a periph reference it never acquired. The sibling ptopen (sys/bus/cam/scsi/scsi_pt.c:154) calls cam_periph_acquire(periph) immediately after fetching periph; sgopen skips this. Every cam_periph_release(periph) call in sgopen (:397, :414) and sgclose (:437) therefore decrements periph->refcount (u_int, init 0 at cam_periph.c:215) below zero, underflowing to 0xFFFFFFFF on the first call. The underflow permanently prevents the periph from being freed via cam_periph_release (the free path at cam_periph.c:372 checks refcount == 1, unreachable after underflow).

Trigger / preconditions

  • Attacker position: local root with SYSCAP_RESTRICTEDROOT (the caps_priv_check_self at :385 requires this).
  • For (1): sysctl kern.securelevel=2 then open("/dev/sg0", O_RDWR) β†’ immediate panic.
  • For (2): any open/close cycle on /dev/sg* at any securelevel silently underflows refcount; observable as CAM_periph allocation growth and broken hot-unplug cleanup.

Reproduction steps (run as root on the guest)

cc -o sg_securelevel_panic sg_securelevel_panic.c
sysctl kern.securelevel=2
./sg_securelevel_panic
# Unpatched: kernel panics here (ssh dies, boot.log shows the trace).
# Patched:   open returns EPERM, guest stays up.

Fix

fix.diff mirrors ptopen: - Adds if (cam_periph_acquire(periph) != CAM_REQ_CMP) return (ENXIO); after fetching periph. - Removes the spurious cam_periph_unlock(periph) from the securelevel > 1 path (the lock was never acquired there).

Now every cam_periph_release in sgopen/sgclose corresponds to the new acquire.

Fix validation (Phase 8)

  • Baseline (with-src snapshot, #0 build, unpatched): reproduced the panic. Captured in panic.txt and run.log.
  • Patched (#1 build at Sun Jul 19 16:49:25 UTC 2026, sha256 bcfe20d5ad4accf44f6020bcb9aaf9162c9a4870a0b5d43022553fd6d2861238):
  • With securelevel=2 + open("/dev/sg0"): open returns EPERM, exit 1, guest stays up. βœ…
  • With securelevel=-1 + open("/dev/sg0"): open succeeds, exit 0 (normal path). βœ…

Fix closes the bug deterministically.

Kernel references

PoC changes

  • sg_securelevel_panic.c written from scratch (no PoC existed in the folder). Adds #include <string.h> to silence the implicit-declaration warning that caused a userspace segfault on the error path (the kernel result was unaffected).

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 16:49:25 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live panic). sgopen unlock-before-lock at securelevel>1 -> lwkt_reltoken panic. Root-only. Kernel rebuild fix.