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)
PoC verification
Evidence pack
findings/poc/DF-2909 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
fixedA/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.
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.
No comments yet.