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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2988 Β· 10 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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_testable

contract comment fixed in fix-all tree; structural cur-tracking left as upstream design discussion

↓ fix.diffper-fix-DF-2988

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.