RB_SCAN concurrency contract overstates the machinery: deleting a node that is another concurrent scan's in-flight element is an unguarded use-after-free
| Field | Value |
|---|---|
| ID | DF-2988 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H |
| CWE | CWE-667 / CWE-670 |
| File | sys/kern/subr_rbtree.c |
| Lines | engine sys/sys/tree.h:302-306, 591-594, 708-712, 777-786 (shims subr_rbtree.c:44-60) |
| 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
tree.h:708-712 documents that 'Any item may be deleted while the scan
is in progress', and subr_rbtree.c:44-50 states RB_SCAN is designed to
run under shared locks. The inprog machinery actually delivers only a
narrower guarantee: (a) the scanning thread may delete its OWN current
element (info.node precomputed before the callback runs), and (b) any
node any scan holds as its pending next element is advanced by the
RB_REMOVE fixup. It does NOT protect a scan's current element (best)
against deletion by a DIFFERENT concurrent scanner: RB_SCAN_INFO
records only node, not cur, so RB_REMOVE cannot fix it up, and the
parked scanner dereferences the freed element inside its callback and
again at scancmp(best)/RB_NEXT(best). A future consumer that takes the
header at its word β two threads in RB_SCAN on the same head under a
shared lock, one callback freeing a node the other is processing β gets
a kernel UAF. No in-tree consumer exhibits the pattern today (all
deleting consumers serialize exclusively: pmap pm_spin, vmobj_token,
teardown paths) β an API trap / latent hazard, not a live exploit.
Unpriv users reach consumers (pmap, vm_object, pf/ipfw state trees),
but exploitation requires a consumer that follows the documented
contract rather than the actual one; today that means future code.
Fix: amend the contract + optionally add cur to RB_SCAN_INFO with an
INVARIANTS KKASSERT in RB_REMOVE (row diff).
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_rbtree.c (GLM 5.3). Engine core (rotations, transplant, rebalance, NULL-sibling cases) proven FreeBSD-identical/correct.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2988 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 724 B | β raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| build.sh | β | 54 B | view raw | |
| dfv_safe.c | β | 21.8 KB | view raw | |
| env.txt | β | 401 B | view raw | |
| fix.diff | β | 980 B | view raw | |
| manifest.json | β | 666 B | view raw | |
| run.log | β | 1.0 KB | view raw | |
| run.sh | β | 123 B | view raw | |
| verdict.json | β | 2.5 KB | view raw |
DF-2988 PoC evidence pack
What
RB_SCAN concurrency contract overstates the machinery: deleting another concurrent scans current element is a UAF.
Build
make (KMOD dfv_safe)
Run
kldload dfv_safe.ko; sysctl -w dfv.rb2988=1; dmesg | grep DF2988
Expected
scanner parked on current element; other thread RB_REMOVE+kfree; scanner re-reads freed node (marker observed in one run)
Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 All logs are full untrimmed captures unless noted. fix.diff is the git-apply-able verified fix (authored after verification; never applied to the audit tree).
DF-2988 - verification verdict
Status: reproduced | reproduced: 1 | impact: corruption | confidence: likely
The documented contract is not honored - demonstrated with a handshake-orchestrated concurrent scan on a live kernel tree. Scanner thread ran RB_SCAN and parked INSIDE its callback holding node X as its current element; a second thread (playing another concurrent scanner under the shared-lock model the header advertises) RB_REMOVEd X and kfree()d it - no fixup occurs because RB_SCAN_INFO records only the pending-NEXT element (tree.h:302-306; RB_REMOVE advances only inprog->node, tree.h:591-594) - then the scanner resumed and dereferenced the freed node, reading its key through the dangling pointer (UAF read executed; log sequence proves ordering). One run additionally observed the reclaimer marker 0x5a5a5a5a through the dangling pointer (freed block recycled by an unrelated allocation before the parked scanner re-read it) - the full freed-then-reused read. tree.h:708-712 advertises Any item may be deleted while the scan is in progress; the machinery only protects the scanners own current element and parked next elements. No in-tree consumer follows the advertised contract (all deleting consumers serialize exclusively) - API trap. Contract comment fixed in the fix-all tree.
Evidence pointers
run.log DF2988 lines: scanner parked holding node ... deleter RB_REMOVEd + kfreed ... scanner resumed; re-reading freed node; plus 0x5a-marker run
Kernel references
PoC changes vs the filing sketch
finding cites subr_rbtree.c:302-786 but the RB machinery lives in sys/sys/tree.h (subr_rbtree.c is 61 lines of spinlock helpers); paths corrected
Fix
recommendation: amend the contract + add cur to RB_SCAN_INFO with INVARIANTS KKASSERT in RB_REMOVE
fix status: not_testable
contract comment fixed in fix-all tree; structural cur-tracking left as upstream design discussion
Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablecontract comment fixed in fix-all tree; structural cur-tracking left as upstream design discussion
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log DF2988 lines: scanner parked holding node ... deleter RB_REMOVEd + kfreed ... scanner resumed; re-reading freed node; plus 0x5a-marker run
PoC changes
finding cites subr_rbtree.c:302-786 but the RB machinery lives in sys/sys/tree.h (subr_rbtree.c is 61 lines of spinlock helpers); paths corrected
Verified recommended fix
amend the contract + add cur to RB_SCAN_INFO with INVARIANTS KKASSERT in RB_REMOVE
Verdict
The documented contract is not honored - demonstrated with a handshake-orchestrated concurrent scan on a live kernel tree. Scanner thread ran RB_SCAN and parked INSIDE its callback holding node X as its current element; a second thread (playing another concurrent scanner under the shared-lock model the header advertises) RB_REMOVEd X and kfree()d it - no fixup occurs because RB_SCAN_INFO records only the pending-NEXT element (tree.h:302-306; RB_REMOVE advances only inprog->node, tree.h:591-594) - then the scanner resumed and dereferenced the freed node, reading its key through the dangling pointer (UAF read executed; log sequence proves ordering). One run additionally observed the reclaimer marker 0x5a5a5a5a through the dangling pointer (freed block recycled by an unrelated allocation before the parked scanner re-read it) - the full freed-then-reused read. tree.h:708-712 advertises Any item may be deleted while the scan is in progress; the machinery only protects the scanners own current element and parked next elements. No in-tree consumer follows the advertised contract (all deleting consumers serialize exclusively) - API trap. Contract comment fixed in the fix-all tree.
No comments yet.