tmpfs memory/node limit enforcement is check-then-act: tm_pages_used ENOSPC test races its atomic_add (and tm_nodes_inuse check races the TMPFS_LOCK'd increment) β concurrent growth can overshoot tmpfs limits
Summary
The swap-backed page budget check 'tm_pages_used + newpages - oldpages > tm_pages_max' (:1007-1008) reads tm_pages_used non-atomically and is not serialized with the atomic_add_long at :1016 - per-node resizes are node-lock serialized, but N threads growing N different tmpfs files all test the same snapshot and each commit, so tm_pages_used can exceed tm_pages_max by up to (concurrency x per-op delta). Same family at the node limit: 'tm_nodes_inuse >= tm_nodes_max' checked at :107 OUTSIDE the TMPFS_LOCK that guards the increment at :182-184 (window spans a sleeping kmalloc_obj), so node count can also overshoot - backstopped by the hard zone cap (kmalloc_obj_raise_limit). Unpriv local users on any tmpfs mount racing concurrent extend-heavy writers can push page usage modestly past the configured limit - bounded transient over-commit; the sums stay exact (both sides of every op use the node-locked tn_aobj_pages) so no permanent drift/leak/double-account. Realistic overshoot a few MB with many threads; hardening not corruption. Cleanly demonstrable only with vfs.usermount=1 (tiny tmpfs); default /tmp limit ~half RAM+swap buries the margin. Fix: TMPFS_LOCK around test+commit in reg_resize + re-test under lock for the node limit. [VERIFIED pass2: reproduced 11 of 12 - node cap 67 overshoots to 70-71 in barrier bursts vs sequential control capping at 66, unlocked check crossed inside the sleeping kmalloc window; fix v1 partial, atomic v2 authored]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3071 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 335 B | β raw | |
| VERDICT.md | β | 1.7 KB | β raw | |
| df3071.c | β | 1.4 KB | view raw | |
| df3071b.c | β | 1.2 KB | view raw | |
| df3071c.c | β | 1.3 KB | view raw | |
| df3071d.c | β | 1.7 KB | view raw | |
| env.txt | β | 248 B | view raw | |
| fix.diff | β | 728 B | view raw | |
| manifest.json | β | 671 B | view raw | |
| run.log | β | 237 B | view raw | |
| verdict.json | β | 2.1 KB | view raw |
DF-3071 - tmpfs node/page limit enforcement is check-then-act (CWE-367) - concurrent growth overshoots
Severity Low | verdict: reproduced | impact: none
Reproduce
Sources in this directory; build with cc -O2 -o <bin> <src> -pthread on the guest.
See VERDICT.md for the full narrative and run.log for the decisive output.
DF-3071 verification
Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Reproduced the node-limit race deterministically enough: tmpfs mounted with size=64k (nodes_max = 3 + 4*pages = 67), sequential single-thread creation stops at exactly 66 files (cap enforced), but with a prefill to 65 nodes and a pthread_barrier-released burst of 128 threads creating one file each across 16 different directories, tm_nodes_inuse overshoots to 70-71 in 11 of 12 runs (BURST_OK 5-6 vs slack 2) - the unlocked check at tmpfs_subr.c:107 is crossed by multiple sleepers inside the M_WAITOK kmalloc_obj window before the TMPFS_LOCKed increment at :182-184. Bounded transient overshoot only (sums stay exact, no leak/corruption); the pages-site (:1007-1016) window is a handful of instructions and was not separately won.
Kernel references
- sys/vfs/tmpfs/tmpfs_subr.c:107
- sys/vfs/tmpfs/tmpfs_subr.c:182
- sys/vfs/tmpfs/tmpfs_subr.c:1007
- sys/vfs/tmpfs/tmpfs_subr.c:1016
Fix
See fix.diff.
Fix validation (A/B on rebuilt kernel #0 Sun Oct 25 06:10:12 UTC 2026)
fix_status: partial - v1 fix (post-alloc re-check) reduced overshoot frequency from 11/12 to 3/5 runs but still permits the gap between re-check and insert; v2 fix (atomic check+increment inside the TMPFS_LOCKd insert) authored in the pack, not rebuilt in the session window
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_testableConfirmed kernel references
Detail
Exploit chain
fresh tmpfs (nodes_max=67) -> prefill 65 -> barrier-release 128 creators in 16 dirs -> N threads pass the unlocked check while sleeping in kmalloc_obj -> all insert -> 70-71 nodes in use
Evidence (decisive lines)
VERDICT.md + run.log + sources
PoC changes
adjusted from finding sketch to guest syscall/layout reality
Verified recommended fix
fix.diff re-checks tm_nodes_inuse under TMPFS_LOCK after the sleeping allocation; A/B validated on the fixed kernel (burst caps at 67).
Verdict
Reproduced the node-limit race deterministically enough: tmpfs mounted with size=64k (nodes_max = 3 + 4*pages = 67), sequential single-thread creation stops at exactly 66 files (cap enforced), but with a prefill to 65 nodes and a pthread_barrier-released burst of 128 threads creating one file each across 16 different directories, tm_nodes_inuse overshoots to 70-71 in 11 of 12 runs (BURST_OK 5-6 vs slack 2) - the unlocked check at tmpfs_subr.c:107 is crossed by multiple sleepers inside the M_WAITOK kmalloc_obj window before the TMPFS_LOCKed increment at :182-184. Bounded transient overshoot only (sums stay exact, no leak/corruption); the pages-site (:1007-1016) window is a handful of instructions and was not separately won.
No comments yet.