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

tmpfs dirent zone keeps the default malloc limit (RAM/10) and tmpfs_alloc_dirent allocates without M_NULLOK: unprivileged link(2) flood on the default /tmp panics the kernel ("tmpfs dirent: malloc limit exceeded")

Summary

tmpfs_mount raises ONLY the node zone limit at mount; the per-mount 'tmpfs dirent' malloc_type keeps the default ks_limit = RAM/10 (390MB on the 4GB guest). tmpfs_alloc_dirent (tmpfs_subr.c:306-307) allocates with M_WAITOK and no M_NULLOK, and the DF allocator PANICS when a zone limit is hit without M_NULLOK (kern_slaballoc.c:877). tmpfs_nlink has no dirent budget (only per-node LINK_MAX), so link(2) fills the zone with 128-byte dirents while consuming zero nodes/pages - neither tm_nodes_max nor tm_pages_max can stop it. VERIFIED stock INVARIANTS guest: unpriv user (uid 1001) link-flooding the default-boot /tmp panicked in ~90s at ~3.1M links with 'panic: tmpfs dirent: malloc limit exceeded', backtrace kern_link->tmpfs_nlink->tmpfs_alloc_dirent->_kmalloc_obj. Any local user on any default-install DF box (tmpfs /tmp standard) kills the machine with ~3.2M link() syscalls and no privileges/config. Fix validated in-guest (raise dirent/name zone limits with the node budget + M_NULLOK/ENOSPC): same flood returns ENOSPC at the same threshold, guest stays up.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3179 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
build.log β€” 61 B view raw
build.sh β€” 68 B view raw
env.txt β€” 413 B view raw
fix.diff β€” 1.3 KB view raw
fix_validation.log β€” 735 B view raw
linkflood.c β€” 2.4 KB view raw
panic.txt β€” 616 B view raw
panic.txt.raw β€” 2.1 KB ↓ download
run.fixed.log β€” 367 B view raw
run.log β€” 267 B view raw
run.sh β€” 91 B view raw
verdict.json β€” 3.3 KB view raw

DF-3179 β€” tmpfs dirent zone keeps the default malloc limit; tmpfs_alloc_dirent panics when it is reached (unprivileged, default /tmp)

What

tmpfs_mount() (sys/vfs/tmpfs/tmpfs_vfsops.c:200-207) creates three per-mount malloc zones and raises ONLY the node zone limit:

kmalloc_create_obj(&tmp->tm_node_zone, "tmpfs node", sizeof(struct tmpfs_node));
kmalloc_create_obj(&tmp->tm_dirent_zone, "tmpfs dirent", sizeof(struct tmpfs_dirent));
kmalloc_create(&tmp->tm_name_zone, "tmpfs name zone");

kmalloc_obj_raise_limit(tmp->tm_node_zone,
            sizeof(struct tmpfs_node) * tmp->tm_nodes_max);

The dirent zone (and name zone) keep the malloc default limit ks_limit = kmem_lim_size()/10 (= RAM/10 β€” sys/kern/kern_slaballoc.c:371-372; 390 MB on the 4 GB test guest).

tmpfs_alloc_dirent() (sys/vfs/tmpfs/tmpfs_subr.c:306-307) allocates the dirent with M_WAITOK and no M_NULLOK, and the allocator hard-panics when a zone limit is exceeded without M_NULLOK:

panic("%s: malloc limit exceeded", type->ks_shortdesc);   /* kern_slaballoc.c:877,
                                                             kern_kmalloc.c:706 */

tmpfs_nlink() (sys/vfs/tmpfs/tmpfs_vnops.c:1170-1241) has no dirent budget check β€” only per-node LINK_MAX β€” so link(2) adds a 128-byte dirent (plus a short name) per call while consuming zero tmpfs nodes and zero pages. Neither tm_nodes_max nor tm_pages_max can stop it.

Impact

Any local user can panic the kernel from the default-boot /tmp tmpfs with a plain link() flood (no vfs.usermount, no privileges, no mount syscall):

panic: tmpfs dirent: malloc limit exceeded
cpuid = 4
_kmalloc_obj() at _kmalloc_obj+0x758
tmpfs_alloc_dirent() at tmpfs_alloc_dirent+0x38
tmpfs_nlink() at tmpfs_nlink+0x11a
vop_nlink() at vop_nlink+0x57
kern_link() at kern_link+0x252

Threshold on the guest: (RAM/10)/128 B β‰ˆ 3,194,880 dirents (panic observed between 3.0M and 3.2M links, ~90 s). With vfs.usermount=1 the same mechanism is available on any number of attacker-created mounts; root can also trigger it on any tmpfs via ordinary file creation when nodes_max > (RAM/10)/sizeof(tmpfs_dirent).

Reproduce

build.sh    # cc -O2 -o linkflood linkflood.c   (in guest)
run.sh      # as an unprivileged user: mkdir /tmp/df3179 && ./linkflood /tmp/df3179

Expected: guest panics with panic: tmpfs dirent: malloc limit exceeded once ~RAM/10/128 links have been created; see run.log + panic.txt.

Fix

fix.diff (validated in-guest): 1. tmpfs_vfsops.c: raise the dirent and name zone limits with the node budget, exactly like the node zone. 2. tmpfs_subr.c: tmpfs_alloc_dirent allocates with M_NULLOK and returns ENOSPC on exhaustion (matching the name allocation directly below it) β€” degrades cleanly instead of panicking regardless of limits.

With the fix, the same flood terminates with link(): No space left on device and the guest stays up (see VERDICT.md fix section).

VERDICT.md
↓ download raw

DF-3179 verdict β€” REPRODUCED (panic, unprivileged, default config)

Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (INVARIANTS), 4.25 GB RAM, 4 GB swap, tmpfs on /tmp (975M, boot default), tmpfs on /var/run/shm.

Baseline run (2026-09-07, user maxx uid 1001, no privileges, no usermount):

$ mkdir -p /tmp/df3179 && cd /tmp/df3179 && ~/linkflood /tmp/df3179
target /tmp/df3179: f_type=134 f_blocks=249856 f_files=999427
links=200000 ... links=3000000
(guest dies)

Serial console (dfbsd-qemu/vm.sh log):

panic: tmpfs dirent: malloc limit exceeded
cpuid = 4
Trace beginning at frame 0xfffff80117f5f568
_kmalloc_obj() at _kmalloc_obj+0x758
tmpfs_alloc_dirent() at tmpfs_alloc_dirent+0x38
tmpfs_nlink() at tmpfs_nlink+0x11a
vop_nlink() at vop_nlink+0x57
kern_link() at kern_link+0x252
Debugger("panic")

Mechanism (source-traced): * tmpfs_vfsops.c:200-204 creates the per-mount "tmpfs dirent" malloc_type; only tm_node_zone gets kmalloc_obj_raise_limit (:206-207). The dirent zone keeps malloc's default ks_limit = kmem_lim_size()/10 (kern_slaballoc.c:365-372) = 390 MB on this guest (confirmed by vmstat -m: "tmpfs_dirent ... Limit 390M"). * tmpfs_subr.c:306-307: kmalloc_obj(..., M_WAITOK) β€” no M_NULLOK β€” so kern_kmalloc.c:703-707 / kern_slaballoc.c:863-878 panic() instead of returning NULL when the limit is hit. * tmpfs_nlink (tmpfs_vnops.c:1170) checks only LINK_MAX (per node); nothing bounds per-mount dirent count. Each link() = 1 dirent (128 B) + ~16 B name; nodes/pages never consumed. * Observed threshold: panic between 3.0M and 3.2M links β€” matches (390 MB / 128 B) = 3,194,880 dirents. * Same bug reachable by root on any tmpfs via creat/mkdir/symlink once nodes_max exceeds (RAM/10)/128 (e.g. explicit nodes=, or default mounts on machines with swap β‰₯ ~1.1x RAM where derived nodes_max > threshold); reachable unprivileged on usermounts (vfs.usermount=1).

Fix validation (fix.diff applied to guest /usr/src, make nativekernel, installed, rebooted): * Patched kernel: same linkflood /tmp/df3179 run as user maxx β†’ link()s proceed past 3.19M and terminate with ENOSPC ("link: No space left on device"), guest STAYS UP, filesystem still usable (create/unlink in a fresh subdirectory works). No panic. * fix.diff = (a) raise dirent+name zone limits with nodes_max at mount (tmpfs_vfsops.c, adjacent to the existing node-zone raise), and (b) tmpfs_alloc_dirent uses M_NULLOK + ENOSPC (tmpfs_subr.c), so no tmpfs zone exhaustion can ever panic regardless of configuration.

Status: reproduced / impact=panic (unprivileged local DoS, default config). Primary deliverable for this class is the fix; no memory-corruption primitive is involved (the panic is a deliberate allocator abort).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

baseline panics at ~3.1M links; patched kernel with fix.diff returns ENOSPC from link(2) at the same threshold, no panic, filesystem remains usable and recovers after dirent free; guest reset to clean snapshot afterwards

['fix_validation.log', 'run.fixed.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 22:45:12 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log (links=...3.0M then guest dies)', 'panic.txt (full serial panic + backtrace tmpfs_alloc_dirent+0x38 <- tmpfs_nlink+0x11a <- kern_link+0x252)', 'run.fixed.log (patched kernel: link(): No space left on device at ~3.12M, no panic)', 'fix_validation.log', 'env.txt']

PoC changes

initial draft needed sys/statfs.h include removed (DragonFly declares statfs in sys/mount.h); otherwise the seeded concept (link flood) worked as-is

Verified recommended fix

raise dirent+name zone limits in proportion to tm_nodes_max at mount and allocate dirents with M_NULLOK returning ENOSPC

Verdict

REPRODUCED on stock INVARIANTS X86_64_GENERIC: an unprivileged user (uid 1001) running a plain link(2) flood in the default-boot /tmp tmpfs panics the kernel with 'tmpfs dirent: malloc limit exceeded' (kern_link -> tmpfs_nlink -> tmpfs_alloc_dirent -> _kmalloc_obj) once the mount's per-mount dirent zone hits its default ks_limit = kmem_lim/10 = 390MB on the 4GB guest (~3.19M dirents; observed between 3.0M and 3.2M links, ~90s). Root cause is mount-side: tmpfs_mount (tmpfs_vfsops.c:200-207) raises ONLY the node zone limit; the dirent zone keeps the malloc default and tmpfs_alloc_dirent (tmpfs_subr.c:306-307) allocates with M_WAITOK and no M_NULLOK, so the allocator PANICs on limit exhaustion instead of returning NULL; tmpfs_nlink has no dirent budget. fix.diff (raise dirent+name zone limits with nodes_max + M_NULLOK/ENOSPC in tmpfs_alloc_dirent) validated in-guest on a rebuilt kernel: same flood terminates with ENOSPC, guest stays up, filesystem recovers after freeing dirents.