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

sysref_activate's negative-space gate is ambiguous between initializing and terminating: an activate during the termination window resurrects the object and drives double (or unbounded) termination with zero layer diagnostics

Field Value
ID DF-2942
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:L
CWE CWE-672
File sys/kern/kern_sysref.c
Lines 278-285, 164, 323
Area kern
Confidence likely
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

sysref_activate's only validation, KASSERT(count < 0 && count + 0x40000001 > 0), accepts count == βˆ’0x40000000 β€” the exact state entered by _sysref_put's 1 β†’ βˆ’0x40000000 termination transition and indistinguishable from the post-sysref_alloc initializing state, because struct sysref has no TERMINATING flag. Verified on the stock INVARIANTS guest: calling sysref_activate() on a terminating object passed the layer KASSERT, resurrected refcnt to +1 with no diagnostic, and the protocol then invoked the class terminate callback a SECOND time (term_count=2). A probe variant re-activating on each termination produced 292 consecutive re-terminations ending in a fatal double fault (kernel stack exhaustion via recursion). Latent, not attacker-reachable in-tree (the only live sysref_activate caller is devfs_new_cdev, invoked exactly once immediately after sysref_alloc). But any future caller that re-activates during teardown gets a blessed resurrection: for the only real class (cdev) a double devfs_cdev_terminate would run devfs_release_ops twice (refcount underflow, double kfree risk) and double-unlock devfs_lock; the unbounded variant is a guaranteed stack-exhaustion panic (demonstrated). Fix: add SRF_TERMINATING, set on the termination transition, required absent in sysref_activate's assert.

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-2942 Β· 12 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.1.unbounded.log β€” 88.4 KB view raw
panic.txt β€” 579 B view raw
env.txt β€” 353 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
fix.diff β€” 2.6 KB view raw
verdict.json β€” 3.9 KB view raw
VERDICT.md
↓ download raw

DF-2942 VERDICT β€” sysref_activate accepts termination-in-progress objects (negative-space ambiguity)

Finding

The sysref protocol uses the same negative refcount space for two mutually-exclusive states:

  • initializing: sysref_alloc() sets refcnt = -0x40000000 (kern_sysref.c:164) and the caller later calls sysref_activate(), which adds 0x40000001 (-0x40000000 -> +1);
  • terminating: _sysref_put()'s 1 -> -0x40000000 cmpset (kern_sysref.c:323) enters termination-in-progress, then calls ops.terminate (:324).

sysref_activate()'s only gate is KASSERT(count < 0 && count + 0x40000001 > 0) (kern_sysref.c:280-281), which is satisfied by a terminating object (-0x40000000 + 0x40000001 = +1 > 0). There is no SRF_TERMINATING distinction, so an activate that lands in the termination window (or any caller that re-activates a terminating object) resurrects the object to +1 while its class teardown is concurrently executing.

Reproduction (guest, stock INVARIANTS kernel #0)

sysref_probe.c (synthetic sysref_class, terminate callback mirroring devfs_cdev_terminate's protocol). Trigger A drives a full lifecycle and calls sysref_activate() from inside the terminate callback, deterministically simulating a racing misuser:

terminate#1 entry refcnt=-1073741824            <- termination-in-progress
DF-2942: sysref_activate() on TERMINATING obj, before=-1073741824
DF-2942: after activate refcnt=1 (RESURRECTED; kern_sysref.c:280 KASSERT passed, no diagnostic)
terminate#2 entry refcnt=-1073741824            <- SECOND termination
A: result term_count=2 refcnt=0
A: CONFIRMED DOUBLE TERMINATION

(run.log:1-13) β€” the layer accepted the activation with zero diagnostics on the INVARIANTS kernel and then invoked the class terminate callback a second time.

Run 1 (probe variant re-activating on every termination) shows the unbounded form: 292 consecutive terminate re-entries with the object bouncing +1 <-> -0x40000000, each cycle nesting sysref_put -> _sysref_put -> ops.terminate -> sysref_put, until the kernel stack exhausted (run.1.unbounded.log: Fatal double fault, panic: double fault).

Why this matters (impact ceiling)

For a real class the second terminate re-executes the teardown that already ran once. For the only in-tree consumer (cdev, devfs_core.c:2487-2507): devfs_cdev_terminate would call devfs_release_ops() a second time (decrementing the ops refcount past zero β€” devfs_core.c:2666 has no floor either β€” and potentially freeing the tracking struct twice), then lockmgr(&devfs_lock, LK_RELEASE) an extra time (lock underflow), then a second terminal sysref_put β€” i.e. a double-terminate converts directly into lock-count and refcount corruption for the class. The unbounded variant is a guaranteed stack-exhaustion panic (demonstrated).

Attacker reachability: none in-tree today. The only live sysref_activate caller is devfs_new_cdev (devfs_core.c:2443), invoked exactly once immediately after sysref_alloc β€” the single-call discipline is trivially maintained. The hazard is latent: the layer permits the misuse and even its INVARIANTS asserts bless it. Filed as Info hardening, confidence certain on the mechanism, speculative on any future exploit path.

Kernel references

Fix

Distinguish the two negative-space states with a flag (see fix.diff): set SRF_TERMINATING on the 1 -> -0x40000000 transition, clear it in sysref_alloc/sysref_ctor, and require (sr->flags & SRF_TERMINATING) == 0 in sysref_activate's assert. Fix authored post-verification; not build-validated (Info hardening finding; the fix's observable effect is the new assert itself).

Fix verification

not_testable

Fix authored post-verification (fix.diff); not build-validated: Info hardening finding, the fix's observable effect is the new assert path itself; no corruption was reproduced to A/B test against.

↓ fix.diffper-fix-DF-2942

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-2942/run.log: 'DF-2942: after activate refcnt=1 (RESURRECTED; kern_sysref.c:280 KASSERT passed, no diagnostic)' followed by terminate#2 and 'A: CONFIRMED DOUBLE TERMINATION (terminate callback ran twice for one object)'.
findings/poc/DF-2942/run.1.unbounded.log: 292 consecutive terminate re-entries then 'Fatal double fault' / 'panic: double fault' (stack exhaustion via sysref_put -> _sysref_put -> ops.terminate recursion).
findings/poc/DF-2942/build.log: clean KLD build (cc 8.3, INVARIANTS kernel #0).
sys/kern/kern_sysref.c:280-281 accepting KASSERT; :164 init sentinel; :323 termination transition; sys/sys/sysref.h:115-117 flag bits (no TERMINATING).

PoC changes

Authored from scratch (no seed): synthetic sysref_class KLD mirroring devfs_cdev_terminate's lock/unlock/terminate protocol; the activate-during-termination call is made from inside the terminate callback (deterministic simulation of a racing misuser) with a one-shot flag; run 1 accidentally demonstrated the unbounded variant before the flag was added (kept as run.1.unbounded.log).

Verified recommended fix

Add an SRF_TERMINATING flag set on the 1 -> -0x40000000 transition and cleared on (re)initialization, and extend sysref_activate's KASSERT to reject terminating objects so the initializing and terminating negative-space states are no longer ambiguous.

Verdict

Mechanism reproduced on the stock INVARIANTS guest via a KLD probe with a synthetic sysref_class. sysref_activate()'s only gate, the KASSERT at kern_sysref.c:280, accepts count == -0x40000000 -- the exact termination-in-progress state entered by _sysref_put's 1 -> -0x40000000 cmpset (:323). Calling sysref_activate() on a terminating object resurrected it to refcnt +1 with zero layer diagnostics on the INVARIANTS kernel, and the protocol then invoked the class terminate callback a SECOND time (run.log: term_count=2, 'CONFIRMED DOUBLE TERMINATION'); a probe variant re-activating on every termination produced 292 consecutive re-terminations ending in a fatal double fault from kernel stack exhaustion (run.1.unbounded.log). For a real class a double terminate re-runs the teardown (cdev's devfs_cdev_terminate would double devfs_release_ops and double-unlock devfs_lock), and the unbounded variant is a guaranteed stack-exhaustion panic. Attacker reachability: none in-tree -- the only live sysref_activate caller is devfs_new_cdev (devfs_core.c:2443), invoked exactly once right after sysref_alloc; the finding is the layer's ambiguous negative space (latent hardening gap, no SRF_TERMINATING distinction).