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.9 KB | β raw | |
| fill.c | β | 841 B | view raw | |
| szprobe.c | β | 843 B | view raw | |
| build.sh | β | 144 B | view raw | |
| run.sh | β | 217 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 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_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.