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)
PoC verification
Evidence pack
findings/poc/DF-2942 Β· 12 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.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 |
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()setsrefcnt = -0x40000000(kern_sysref.c:164) and the caller later callssysref_activate(), which adds0x40000001(-0x40000000 -> +1); - terminating:
_sysref_put()'s1 -> -0x40000000cmpset (kern_sysref.c:323) enters termination-in-progress, then callsops.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
sys/kern/kern_sysref.c:278-285βsysref_activateand the accepting KASSERT (:280)sys/kern/kern_sysref.c:164β initialization sets the same-0x40000000sentinelsys/kern/kern_sysref.c:314-327β termination entry (1 -> -0x40000000at:323,ops.terminateat:324)sys/sys/sysref.h:107-118βstruct sysrefflags: no TERMINATING bitsys/vfs/devfs/devfs_core.c:2443β the only in-tree activate callsys/vfs/devfs/devfs_core.c:2487-2507β real-class terminate that a double invocation corrupts
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_testableFix 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.
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).
No comments yet.