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_RESTRICTEDROOTis required bysgopenline 385). - Privileges gained or impact:
- Lock-state corruption panic (immediate): triggered by any root-level process calling
open()on/dev/sg*whensecurelevel > 1. In hardened server configurations that setsecurelevel > 1, any administrative tool, backup agent, or monitoring daemon that probes SCSI devices triggers an immediate kernel panic (lockmgrassertion 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
sgconfigured. Root to trigger. - Reachability:
open("/dev/sg0", O_RDWR)as root. Thesecurelevel > 1panic requiressysctl 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.
Recommended fix
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
sys/bus/cam/scsi/scsi_sg.c:388-418βsgopen(no acquire; unlock without lock)sys/bus/cam/scsi/scsi_sg.c:421-441βsgclose(release without acquire)sys/bus/cam/scsi/scsi_pt.cβptopenshows the correct acquire-then-release patternsys/bus/cam/cam_periph.c:215, 341-378β refcount init (0) and release-or-free logicsys/bus/cam/cam_sim.c:64-70βcam_periph_unlockcallslockmgr(LK_RELEASE)- CWE-667 Improper Locking; CWE-911 Improper Update of Reference Count
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1051 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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(thecaps_priv_check_selfat:385requires this). - For (1):
sysctl kern.securelevel=2thenopen("/dev/sg0", O_RDWR)β immediate panic. - For (2): any
open/closecycle 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-srcsnapshot,#0build, unpatched): reproduced the panic. Captured inpanic.txtandrun.log. - Patched (
#1build atSun Jul 19 16:49:25 UTC 2026, sha256bcfe20d5ad4accf44f6020bcb9aaf9162c9a4870a0b5d43022553fd6d2861238): - With
securelevel=2+open("/dev/sg0"): open returnsEPERM, exit 1, guest stays up. β - With
securelevel=-1+open("/dev/sg0"): open succeeds, exit 0 (normal path). β
Fix closes the bug deterministically.
Kernel references
sys/bus/cam/scsi/scsi_sg.c:388-418β sgopen (no acquire; unlock without lock)sys/bus/cam/scsi/scsi_sg.c:421-441β sgclose (release without acquire)sys/bus/cam/scsi/scsi_pt.c:138-178β ptopen (correct acquire-then-release pattern)sys/bus/cam/cam_periph.c:215, 319-329, 341-378β refcount init, acquire, release/freesys/kern/lwkt_token.cβlwkt_reltokenillegal-release panic (the actual fault)
PoC changes
sg_securelevel_panic.cwritten 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
fixedvalidated
see evidence pack
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.
No comments yet.