kmalloc_obj zone-limit accounting uses cacheline-aligned ks_objsize while callers must derive limits from unaligned sizeof β tmpfs inode quota exhausts (ENOSPC) at ~85% of the configured cap
| Field | Value |
|---|---|
| ID | DF-2819 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-190 / CWE-682 (unit-mismatch in limit arithmetic) |
| File | sys/kern/kern_kmalloc.c |
| Lines | 899 (accounting), 694-708 (enforcement); consumer tmpfs_vfsops.c:206-207 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
_kmalloc_obj accounts each object at ks_objsize = __VM_CACHELINE_ALIGN(sizeof) and refuses once Ξ£ memuse β₯ ks_limit. ks_objsize is private (no accessor), so tmpfs's raise β the only count-derived zone limit in the tree β uses unaligned sizeof(struct tmpfs_node) Γ tm_nodes_max. Measured: sizeof=272, accounted=320 (48B/ node, 17.6%). A tmpfs mounted with inodes=1600000 raises the limit to 435,200,000B but the zone refuses at kΓ320 β₯ limit β k=1,360,000 (85.02%). Default-mounted /tmp equally affected. tmpfs allocates with M_NULLOK so the failure is graceful ENOSPC, not the panic.
Proof of contest
VERIFIED on the guest (findings/poc/DF-2819/): tmpfs
inodes=1600000; unpriv fill loop β stopped at 1360369: No space left
on device (predicted 1,360,000; offset = root node + flush
granularity), vmstat -m showing tmpfs_node MemUse==Limit==415M while
tm_nodes_inuse < tm_nodes_max and zero data pages used. Fix (align the
tmpfs raise with __VM_CACHELINE_ALIGN; longer term export
kmalloc_obj_objsize()) β fixed-threshold arithmetic exact; not
kernel-validated (Low, non-corruption).
Recommended fix
See findings/poc/DF-2819/fix.diff.
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_kmalloc.c (GLM 5.3); 85%-cap exhaustion reproduced with exact arithmetic same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2819 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| fill.c | β | 841 B | view raw | |
| szprobe.c | β | 843 B | view raw | |
| build.sh | β | 320 B | view raw | |
| run.sh | β | 480 B | view raw | |
| run.log | β | 267 B | view raw | |
| vmstat.txt | β | 664 B | view raw | |
| env.txt | β | 307 B | view raw | |
| fix.diff | β | 718 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-2819 β kmalloc_obj zone-limit accounting uses cacheline-aligned objsize while callers derive limits from unaligned sizeof β tmpfs inode quota exhausted at ~85% of configured cap
What this is
Reproduction of a resource-accounting defect in the kmalloc_obj zone
allocator (sys/kern/kern_kmalloc.c). The allocator accounts per-object
memory use as the cache-line-aligned ks_objsize
(__VM_CACHELINE_ALIGN(objsize), applied at zone creation in
_kmalloc_create_obj, sys/kern/kern_slaballoc.c:610; accounting at
sys/kern/kern_kmalloc.c:899 use->memuse += size), but callers cannot
learn ks_objsize (no accessor exists), so the only caller that derives a
zone limit from an object count β tmpfs β computes it from the
unaligned sizeof(struct tmpfs_node) * tm_nodes_max
(sys/vfs/tmpfs/tmpfs_vfsops.c:206-207).
sizeof(struct tmpfs_node) = 272, aligned objsize = 320
(measured on the guest with a KLD probe: SZPROBE node=272 nodealign=320).
The zone therefore "fills" at 272/320 = 85% of the promised inode count.
How to reproduce (as performed on the QEMU guest)
- Build
szprobeKLD (optional, proves sizes):cd /root/szprobe && makeβkldload ./szprobe.koβ dmesg showsSZPROBE node=272 nodealign=320 dirent=88 direntalign=128. - Mount a tmpfs whose raised limit exceeds the default floor
(
kmem_lim_size()/10= 390 MiB on this guest):mount -t tmpfs -o size=8g,inodes=1600000 none /mnt/tfillβ raised limit = 1,600,000 Γ 272 = 435,200,000 B = 415.04 MiB. - As an unprivileged user (
nobody), create empty files until failure:/mnt/tfill/fill /mnt/tfill 1500000(seefill.c). - Expected (observed): ENOSPC at 1,360,369 files β exactly the first
kwherek Γ 320 β₯ 435,200,000(k = 1,360,000) β i.e. 85.02% of the 1,600,000-inode cap.vmstat -mat that moment showstmpfs_node MemUse 415M Limit 415M(memuse == limit). - tmpfs's own inode cap never comes into play (
tm_nodes_inuse= 1.36M < 1.6M), so the filesystem reports ENOSPC while it still advertises ~240K free inodes.
Files
fill.cβ file-creation loop (the trigger), built withcc -O2szprobe.cβ KLD printingsizeof(struct tmpfs_node)and its cacheline alignmentrun.logβ decisive run: progress lines +stopped at 1360369: No space left on devicevmstat.txtβvmstat -mafter the run:tmpfs_nodeMemUse == Limitenv.txtβ guest uname, mount, compiler, sysctlsfix.diffβ one-line fix (tmpfs raises the limit using the aligned size)VERDICT.md,manifest.json,verdict.json
Impact
Quota-correctness / availability: any tmpfs (including default-mounted
/tmp β the guest's /tmp has a default node cap of ~3.14M and would
ENOSPC at ~2.67M files) delivers only ~85% of its configured/default
inode budget. The allocation failure path is graceful (M_NULLOK at
sys/vfs/tmpfs/tmpfs_subr.c:110-111 β ENOSPC), so there is no panic and
no memory-safety impact; severity Low.
DF-2819 VERDICT
Status: reproduced (impact: none/minor availability β quota correctness; no panic, no memory-safety impact) Severity: Low Β· Confidence: certain
Root cause
The kmalloc_obj zone allocator accounts memory use per object as the cache-line-aligned object size:
_kmalloc_create_obj()(sys/kern/kern_slaballoc.c:610) setstype->ks_objsize = __VM_CACHELINE_ALIGN(objsize);_kmalloc_obj()(sys/kern/kern_kmalloc.c:899) doesuse->memuse += sizewithsize == type->ks_objsize(KKASSERT at kern_kmalloc.c:715);- the limit gate (sys/kern/kern_kmalloc.c:694-708) refuses allocation once
Ξ£ ks_use[].memuse >= type->ks_limit(NULL withM_NULLOK, elsepanic("malloc limit exceeded")).
ks_objsize is private to the allocator β there is no accessor β so the
one caller that derives a zone limit from an object count, tmpfs
(sys/vfs/tmpfs/tmpfs_vfsops.c:206-207), uses the unaligned
sizeof(struct tmpfs_node) * tm_nodes_max. On this kernel
(guest-measured via KLD probe):
sizeof(struct tmpfs_node)= 272- accounted per node (
ks_objsize) = 320 (cacheline 64)
Every node therefore burns 320 B of quota against a 272 B budget: the zone limit binds at 272/320 = 85% of the configured inode cap.
Reproduction (QEMU guest, DragonFly 6.5-DEVELOPMENT #0, INVARIANTS kernel)
mount -t tmpfs -o size=8g,inodes=1600000 none /mnt/tfillβ zone limit raised to 1,600,000 Γ 272 = 435,200,000 B = 415.04 MiB (exceeds the malloc_init floorkmem_lim_size()/10= 390 MiB, so the raise binds).- Predicted refusal point: first
kwithk Γ 320 β₯ 435,200,000βk = 1,360,000(85.0% of cap). - Unprivileged user (
nobody) runsfill.ccreating empty files. - Observed:
stopped at 1360369: No space left on device(run.log) β within 369 files of the exact prediction (offset = root node +ks_loosememuseflush granularity KMALLOC_LOOSE_SIZE = 512 KiB, kern_kmalloc.c:901). vmstat -mat the stop point:tmpfs_node 1.30M 415M 428M 415Mβ MemUse (415M) == Limit (415M); Count 1.30M == requests 1.30M (vmstat.txt). Thetmpfs_noderow of an idle 1-node tmpfs (/var/run/shm) shows1 320β per-object accounting is 320 B, direct confirmation of the aligned-unit accounting.- tmpfs's own cap never engaged:
tm_nodes_inuse= 1,360,369+1 <tm_nodes_max= 1,600,000; zero data pages used. The ENOSPC came solely from the zone limit (M_NULLOKat tmpfs_subr.c:110-113).
Why ENOSPC and not a panic: tmpfs allocates nodes with
M_WAITOK | M_ZERO | M_NULLOK, so the over-reached limit returns NULL
β ENOSPC. (Callers without M_NULLOK would panic("malloc limit
exceeded") at kern_kmalloc.c:706 β no practical unprivileged path to
that exists today: the remaining kmalloc_obj zones either are never
raised (hammer2 mchain/mio, nfs mnode β default 390 MiB floor, needs
~1.3M live kernel objects) or pass M_NULLOK.)
Impact
Any tmpfs β including default-mounted /tmp (default node cap β 3.14M
on this 4 GiB guest β real capacity only β 2.67M files) β delivers ~85%
of its advertised inode quota. This is a quota/availability correctness
bug (users get fewer inodes than configured), not a memory-safety bug;
it is also a silent API trap for every future caller that computes zone
limits from sizeof.
Fix
fix.diff (authoritative, caller-side one-liner): tmpfs raises the node
zone limit using the same cacheline alignment the allocator will apply:
- kmalloc_obj_raise_limit(tmp->tm_node_zone,
- sizeof(struct tmpfs_node) * tmp->tm_nodes_max);
+ kmalloc_obj_raise_limit(tmp->tm_node_zone,
+ __VM_CACHELINE_ALIGN(sizeof(struct tmpfs_node)) *
+ tmp->tm_nodes_max);
Arithmetic check of the fix: limit becomes 1.6M Γ 320 = 512 MiB; the
k-th allocation checks ttl = (k-1) Γ 320 β₯ 512 MiB βΊ k β₯ N+1, and
tmpfs refuses at tm_nodes_inuse β₯ N first (tmpfs_subr.c:107), so the
zone limit can never bind before the filesystem's own cap.
(Longer-term: export ks_objsize β e.g. a kmalloc_obj_objsize() β or
make limit accounting use the requested size.)
Fix validation: not performed (kernel rebuild + 1.6M-file re-run is
disproportionate for a Low, non-corruption finding); the fixed threshold
arithmetic above is exact. fix_status: not_testable with this
justification.
Negative result context
The panic variant of this limit gate (kern_kmalloc.c:706) was chased
and is not reachable through tmpfs (M_NULLOK); see the audit JSON
negative notes for the full list of killed candidates in this file.
Fix verification
not_testableFix not kernel-validated (Low non-corruption finding; nativekernel rebuild + 1.6M-file re-run disproportionate). Fixed-threshold arithmetic proven exact: with limit N320 the k-th alloc checks (k-1)320 >= N*320 iff k >= N+1, and tmpfs refuses at tm_nodes_inuse >= N (tmpfs_subr.c:107) first, so the zone limit can never bind before the fs cap.
['fix.diff']
Confirmed kernel references
Detail
Evidence (decisive lines)
["run.log: 'stopped at 1360369: No space left on device' (k*320 >= 1600000*272 first holds at k=1360000)", "vmstat.txt: tmpfs_node row '1.30M 415M 428M 415M 1.30M' (MemUse==Limit); idle 1-node tmpfs row shows per-object accounting of exactly 320 bytes", "env.txt + szprobe dmesg: 'SZPROBE node=272 nodealign=320 dirent=88 direntalign=128'", 'VERDICT.md: full derivation, offset analysis (root node + KMALLOC_LOOSE_SIZE flush granularity)']
PoC changes
Authored trigger from scratch (fill.c file-creation loop as unprivileged user; szprobe KLD to measure sizeof/alignment in the running kernel); no seed PoC existed.
Verified recommended fix
tmpfs: raise the node-zone limit with __VM_CACHELINE_ALIGN(sizeof(struct tmpfs_node)) * tm_nodes_max (see fix.diff); longer term export ks_objsize to callers.
Verdict
Reproduced exactly: the kmalloc_obj node zone for tmpfs refuses allocation (ENOSPC via M_NULLOK at tmpfs_subr.c:110-113) once sum(ks_use[].memuse) reaches the raised limit, but memuse is accounted in cacheline-aligned units (ks_objsize=320B vs sizeof(struct tmpfs_node)=272B, guest-measured via KLD probe), so a tmpfs mounted with inodes=1600000 filled at 1,360,369 files (85.02%, predicted 1,360,000) with vmstat -m showing tmpfs_node MemUse==Limit==415M while tm_nodes_inuse(1.36M) < tm_nodes_max(1.6M). No panic (tmpfs passes M_NULLOK), no memory-safety impact; the defect is quota correctness/availability (every tmpfs, including default /tmp, delivers ~85% of its advertised inode budget) plus an API trap: ks_objsize is not exported so any caller computing count*sizeof limits hits it.
No comments yet.