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

_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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2941 Β· 13 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 β†’ put cycle 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_PUTAWAY to emulate the exact state a racing put sees between :346 and :348, then one extra sysref_put(). Observed: run.log:6 "B2: layer ACCEPTED double-release: refcnt=-1 (walked 0 -> -1; count > -0x40000000 branch, 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, where KKASSERT(sr->refcnt == 0) (kern_sysref.c:163) fires on debug builds and is overwritten silently on production builds). Reaching a second objcache_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

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_testable

Fix 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.

↓ fix.diffper-fix-DF-2941

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).