β¬’ 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.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)

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