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)
PoC verification
Evidence pack
findings/poc/DF-3179 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
fixedbaseline 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']
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.
No comments yet.