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

sys_varsym_set replace is a non-atomic remove-then-insert: concurrent same-name sets create duplicate shadowed varsym entries; invalid level values silently return success

Field Value
ID DF-2909
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:N
CWE CWE-367 / CWE-754
File sys/kern/kern_varsym.c
Lines 152-175 (esp. :168-173)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

A 'set' operation is two independent lock cycles β€” varsymmake(level, name, NULL) to remove, then varsymmake(level, name, buf) to insert β€” with no atomicity between them. Two threads (or two processes sharing a USER-level set) setting the same name concurrently can both pass the remove step and both insert, leaving duplicate entries for one name in the TAILQ. Lookup/list return the first entry; each unset removes only one copy, so a duplicate survives as a shadowing 'zombie' value. Accounting stays symmetric, so NOT memory corruption. Separately, switch(level) has no default case, so level values outside 1..4 return 0 (success) while doing nothing β€” silent API misuse instead of EINVAL. Same-credential confusion only. Fix: atomic replace inside varsymmake (single lock cycle) + default: EINVAL.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_varsym.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2909 Β· 11 files
FileTypeDescriptionSize
README.md β€” 633 B ↓ raw
VERDICT.md β€” 1.9 KB ↓ raw
build.sh β€” 66 B view raw
df2909_runs.log β€” 297 B view raw
df2909_varsymrace.c β€” 2.4 KB view raw
env.txt β€” 401 B view raw
fix.diff β€” 217 B view raw
fix_run.log β€” 92 B view raw
manifest.json β€” 593 B view raw
run.sh β€” 78 B view raw
verdict.json β€” 2.1 KB view raw

DF-2909 PoC evidence pack

What

sys_varsym_set replace is a non-atomic remove-then-insert; invalid levels silently succeed.

Build

cc -o df2909 df2909_varsymrace.c

Run

as unpriv: ./df2909

Expected

3/3 runs: DUPLICATE CONFIRMED (value survives a single unset); invalid level returns 0

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-2909 - verification verdict

Status: reproduced | reproduced: 1 | impact: corruption | confidence: certain

Race reproduced 3/3 runs by unpriv user maxx. Four same-uid processes raced sys_varsym_set(VARSYM_USER, one name) 60000 iterations each; afterwards ONE unset (varsym_set USER name NULL) was issued and varsym_get STILL resolved the name (p2-59997 / p3-59997) - a duplicate shadowed TAILQ entry survived the single delete exactly as claimed (set is two independent lock cycles: varsymmake(name,NULL) remove then varsymmake(name,buf) insert, kern_varsym.c:169-170). Second claim also live-confirmed: varsym_set with level=99 returns 0 silently doing nothing (no default case). Same-credential confusion only; accounting symmetric; no memory unsafety. Fix (default: EINVAL) A/B validated on the fix-all kernel; the atomic-replace half is a larger refactor documented in the pack.

Evidence pointers

df2909_runs.log: DUPLICATE CONFIRMED after one unset, 3 runs; invalid-level set rc=0

Kernel references

PoC changes vs the filing sketch

fixed varsym_get mask semantics (bitmask 1<<level, not level; returns 0 not length)

Fix

recommendation: atomic replace inside one varsymmake lock cycle + default: EINVAL

fix status: fixed

EINVAL default A/B on fix-all kernel; atomic-replace refactor left as follow-up (duplicates still possible with the minimal fix; behavior change for invalid level verified)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/B on fix kernel #2: invalid level returns EINVAL (stock rc=0); varsym set/get regression-checked functional. Minimal fix covers the EINVAL half; atomic-replace refactor still needed for the duplicate-shadowing half.

↓ fix.diffper-fix-DF-2909

Confirmed kernel references

Detail

Evidence (decisive lines)

df2909_runs.log: DUPLICATE CONFIRMED after one unset, 3 runs; invalid-level set rc=0

PoC changes

fixed varsym_get mask semantics (bitmask 1<<level, not level; returns 0 not length)

Verified recommended fix

atomic replace inside one varsymmake lock cycle + default: EINVAL

Verdict

Race reproduced 3/3 runs by unpriv user maxx. Four same-uid processes raced sys_varsym_set(VARSYM_USER, one name) 60000 iterations each; afterwards ONE unset (varsym_set USER name NULL) was issued and varsym_get STILL resolved the name (p2-59997 / p3-59997) - a duplicate shadowed TAILQ entry survived the single delete exactly as claimed (set is two independent lock cycles: varsymmake(name,NULL) remove then varsymmake(name,buf) insert, kern_varsym.c:169-170). Second claim also live-confirmed: varsym_set with level=99 returns 0 silently doing nothing (no default case). Same-credential confusion only; accounting symmetric; no memory unsafety. Fix (default: EINVAL) A/B validated on the fix-all kernel; the atomic-replace half is a larger refactor documented in the pack.