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

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).

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2819 Β· 12 files
FileTypeDescriptionSize
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)

  1. Build szprobe KLD (optional, proves sizes): cd /root/szprobe && make β†’ kldload ./szprobe.ko β†’ dmesg shows SZPROBE node=272 nodealign=320 dirent=88 direntalign=128.
  2. 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.
  3. As an unprivileged user (nobody), create empty files until failure: /mnt/tfill/fill /mnt/tfill 1500000 (see fill.c).
  4. Expected (observed): ENOSPC at 1,360,369 files β€” exactly the first k where k Γ— 320 β‰₯ 435,200,000 (k = 1,360,000) β€” i.e. 85.02% of the 1,600,000-inode cap. vmstat -m at that moment shows tmpfs_node MemUse 415M Limit 415M (memuse == limit).
  5. 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 with cc -O2
  • szprobe.c β€” KLD printing sizeof(struct tmpfs_node) and its cacheline alignment
  • run.log β€” decisive run: progress lines + stopped at 1360369: No space left on device
  • vmstat.txt β€” vmstat -m after the run: tmpfs_node MemUse == Limit
  • env.txt β€” guest uname, mount, compiler, sysctls
  • fix.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.

VERDICT.md
↓ download raw

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) sets type->ks_objsize = __VM_CACHELINE_ALIGN(objsize);
  • _kmalloc_obj() (sys/kern/kern_kmalloc.c:899) does use->memuse += size with size == 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 with M_NULLOK, else panic("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)

  1. 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 floor kmem_lim_size()/10 = 390 MiB, so the raise binds).
  2. Predicted refusal point: first k with k Γ— 320 β‰₯ 435,200,000 β†’ k = 1,360,000 (85.0% of cap).
  3. Unprivileged user (nobody) runs fill.c creating empty files.
  4. Observed: stopped at 1360369: No space left on device (run.log) β€” within 369 files of the exact prediction (offset = root node + ks_loosememuse flush granularity KMALLOC_LOOSE_SIZE = 512 KiB, kern_kmalloc.c:901).
  5. vmstat -m at the stop point: tmpfs_node 1.30M 415M 428M 415M β€” MemUse (415M) == Limit (415M); Count 1.30M == requests 1.30M (vmstat.txt). The tmpfs_node row of an idle 1-node tmpfs (/var/run/shm) shows 1 320 β€” per-object accounting is 320 B, direct confirmation of the aligned-unit accounting.
  6. 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_NULLOK at 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_testable
baseline reproduced→ patch + rebuild →patched clean

Fix 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']
↓ fix.diffper-fix-DF-2819

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.