_sysref_put accepts count==0 double-releases with no put-side floor guard; the only barrier is the debug-only KKASSERT whose flags read races the final-release sequence
| Field | Value |
|---|---|
| ID | DF-2941 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:N |
| CWE | CWE-672 |
| File | sys/kern/kern_sysref.c |
| Lines | 303, 329-336, 346-348 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The count > -0x40000000 branch in _sysref_put also accepts count==0,
so a double-release of a fully-released object is silently decremented
0 β β1 with no structural rejection. The only barrier is the debug-only
KKASSERT at :303, and it reads sr->flags non-atomically with respect
to the final-release sequence (refcnt cmpset β0x40000000β0 at :346
followed by the plain flags |= SRF_PUTAWAY RMW at :348), so a put
landing in that window passes the assert and is accepted EVEN on
INVARIANTS kernels; production builds compile the assert out entirely.
Verified on the stock INVARIANTS guest with a KLD probe (synthetic
sysref_class): fault-injected pre-:348 visibility made the layer accept
the double-release (refcnt walked 0ββ1, logged live), and a real
post-putaway double-put panicked exactly at :303. Put-side twin of the
known get-side DF-0169. Any future kernel-side double-release caller
bug is silently swallowed; refcount driven into permanent negative
limbo (detection only at chunk recycle, debug-only). No unprivileged
or syscall-reachable trigger in-tree today. Fix: explicit count==0
reject branch (KKASSERT + break).
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_sysref.c (GLM 5.3); mechanism proven with privileged KLD probe.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2941 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sysref_probe.c | β | 6.7 KB | view raw | |
| Makefile | β | 64 B | β download | |
| build.sh | β | 537 B | view raw | |
| run.sh | β | 598 B | view raw | |
| build.log | β | 854 B | view raw | |
| run.log | β | 2.1 KB | view raw | |
| run.full_serial.log | β | 2.9 KB | view raw | |
| run.1.unbounded.log | β | 88.4 KB | view raw | |
| panic.txt | β | 579 B | view raw | |
| env.txt | β | 353 B | view raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| fix.diff | β | 1.5 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw |
DF-2941 VERDICT β put-side count==0 acceptance in _sysref_put
Finding
_sysref_put() (sys/kern/kern_sysref.c:297-360) has no floor guard on
the put side. The branch
} else if (count > -0x40000000) { /* kern_sysref.c:329 */
if (atomic_cmpset_int(&sr->refcnt, count, count - 1))
also accepts count == 0, so a double-release (a second sysref_put() for
a reference that was already dropped) silently decrements a fully-released
object 0 -> -1 -> -2 -> ... with no structural rejection. The only
barrier is the debug-only KKASSERT((sr->flags & SRF_PUTAWAY) == 0) at
kern_sysref.c:303 (and the same assert inlined in sysref_put,
sys/sysref2.h:85) β and that assert reads sr->flags non-atomically
with respect to the release sequence in the final branch:
if (atomic_cmpset_int(&sr->refcnt, count, 0)) { /* :346 count -> 0 */
KKASSERT(sr->flags & SRF_ALLOCATED);
sr->flags |= SRF_PUTAWAY; /* :348 flags RMW */
A double-put racing into _sysref_put between :346 and :348 observes
refcnt == 0 with SRF_PUTAWAY not yet visible, passes the entry assert,
and is accepted by the :329 branch β even on INVARIANTS kernels.
After :348 the assert does catch it, but only on debug builds; production
kernels compile KKASSERT out entirely and accept every double-release
silently.
Reproduction (guest, stock INVARIANTS kernel #0)
sysref_probe.c defines a synthetic sysref_class (lockmgr-based
lock/unlock, terminate callback mirroring devfs_cdev_terminate's
protocol: unlock, then drop the terminal ref) and drives the layer:
- B1 β normal
sysref_alloc β sysref_activate β get β put β putcycle completes to putaway (refcnt=0,flags=0x6=SRF_ALLOCATED|SRF_PUTAWAY). Run log:run.log:4"B1: normal cycle refcnt=0 flags=0006 term_count=1 (putaway)". - B2 β fault injection clearing
SRF_PUTAWAYto emulate the exact state a racing put sees between:346and:348, then one extrasysref_put(). Observed:run.log:6"B2: layer ACCEPTED double-release: refcnt=-1 (walked 0 -> -1;count > -0x40000000branch, no floor guard)" β the underflow was accepted on the INVARIANTS kernel. State was then repaired by the probe. - B3 β a real post-putaway double-put (no injection). Observed:
panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in _sysref_put at /usr/src/sys/kern/kern_sysref.c:303(panic.txt, backtrace_sysref_put β sysref_probe_modevent β linker_load_module).
Impact assessment (honest ceiling)
- The layer offers no structural defense against double-releases; the debug-only assert is the whole barrier and is racy w.r.t. its own release sequence.
- Consequences of an accepted double-release on a production kernel: the
object's refcount is driven into permanent negative limbo (the object
has already terminated and been returned to the objcache magazine; the
spurious negative count is only observable at the next
sysref_alloc()of the chunk, whereKKASSERT(sr->refcnt == 0)(kern_sysref.c:163) fires on debug builds and is overwritten silently on production builds). Reaching a secondobjcache_put()of the same object requires ~2^30 additional puts, so the realistic ceiling of a single double-release is silent state corruption / loss of detection, not an immediate double-free. - Attacker reachability: none found in-tree. No syscall path performs
an unpaired
sysref_put(); the finding is that any future caller bug of that shape is silently swallowed by the layer on production builds instead of being caught at the earliest point. This is why the finding is filed as Info (hardening), severity consistent with the existing DF-0169 (get-side overflow guard absence).
Kernel references
sys/kern/kern_sysref.c:305-336β the accepting branchsys/kern/kern_sysref.c:345-355β final release;:346cmpset β:348non-atomicflags |= SRF_PUTAWAYwindowsys/kern/kern_sysref.c:303β the only guard (debug-only, racy)sys/sys/sysref2.h:79-88β inlinesysref_putentry assert (same)sys/kern/kern_sysref.c:163β recycle-time detection point
Fix
Reject count == 0 loudly in _sysref_put (see fix.diff): a dedicated
branch for count == 0 that panics on INVARIANTS and, on all builds,
does not decrement β this converts the silent underflow into a hard,
immediately-diagnosable failure at the point of misuse. Fix authored
post-verification; not build-validated (Info hardening finding; the fix
is assert/plumbing-level and the behavior change is the panic itself).
Fix verification
not_testableFix authored post-verification (fix.diff); not build-validated: Info hardening finding, the observable behavior change of the fix is the panic/no-op itself, and no memory corruption was reproduced to A/B test against.
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-2941/run.log: 'B2: layer ACCEPTED double-release: refcnt=-1 (walked 0 -> -1; `count > -0x40000000` branch, no floor guard)' -- silent acceptance on the INVARIANTS kernel. findings/poc/DF-2941/panic.txt: panic at kern_sysref.c:303 from the un-injected post-putaway double-put (backtrace _sysref_put <- sysref_probe_modevent <- linker_load_module) -- the only guard is debug-only. findings/poc/DF-2941/run.log: 'B1: normal cycle refcnt=0 flags=0006 term_count=1 (putaway)' -- baseline protocol cycle. findings/poc/DF-2941/build.log: clean KLD build (cc 8.3, INVARIANTS kernel #0). sys/kern/kern_sysref.c:329-336 accepting branch; :345-355 final release with the :346->:348 non-atomic flags window; :163 recycle-time KKASSERT.
PoC changes
Authored from scratch (no seed): synthetic sysref_class KLD mirroring devfs_cdev_terminate's lock/unlock/terminate protocol; trigger B2 uses fault injection (clear SRF_PUTAWAY) to emulate the pre-:348 visibility a racing double-put sees, since the genuine window is ~instruction-width; repaired the injected state afterwards so B3 could run in the same guest boot.
Verified recommended fix
Add an explicit count==0 terminal branch to _sysref_put that refuses to decrement (KKASSERT loudly on INVARIANTS builds) instead of letting double-releases silently underflow through the count > -0x40000000 branch.
Verdict
Mechanism reproduced on the stock INVARIANTS guest via a KLD probe with a synthetic sysref_class. _sysref_put's count > -0x40000000 branch (kern_sysref.c:329-336) accepts count==0: a double-release walked a fully-released object 0 -> -1 with no floor guard, after fault-injecting the exact state (SRF_PUTAWAY not yet visible) that a racing put observes between the refcnt cmpset at :346 and the flags RMW at :348 -- accepted even on the INVARIANTS kernel (run.log 'B2: layer ACCEPTED double-release: refcnt=-1'). A real post-putaway double-put then panicked at the layer's only guard, the debug-only KKASSERT at kern_sysref.c:303 (panic.txt: 'assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in _sysref_put'), proving the barrier is debug-only; production builds compile it out and take the B2 path silently. Attacker reachability: none found in-tree -- triggering requires a kernel-side double-release caller bug (none exists among sysref users); the finding is that the layer converts any such future bug into a silent refcount underflow instead of detecting it (Info hardening, same family as the get-side DF-0169).
No comments yet.