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

tmpfs usermount memory cap is attacker-declared: size_max is never clamped to pages_limit for non-root mounts (vfs.usermount=1 β†’ unbounded tm_pages_max/tm_nodes_max)

Summary

tmpfs_mount computes the machine cap pages_limit = vm_swap_max + v_page_count/2 (:157) but only applies it (halved) to the size_max==0 default; the explicit-size path assigns pages = OFF_TO_IDX(size_max) in BOTH branches (:163-173 - identical code, the comment admits the clamp was dropped for root fstab boot-order reasons). Usermounts are not distinguished: with vfs.usermount=1 any user mounting on a self-owned directory (SYSCAP_NOMOUNT_TMPFS under __SYSCAP_NOROOTTEST, default creds unrestricted) sets tm_pages_max up to 2^51 pages and the derived tm_nodes_max likewise, so the ENOSPC guard in tmpfs_reg_resize never fires and the only backstop is machine-wide memory exhaustion. VERIFIED: uid 1001 mount with ta_size_max=1TiB reported f_blocks=268435456 pages (1024 GiB) vs the ~3GiB machine cap on the 4GB/4GB guest (341x). Unpriv whole-system memory-exhaustion DoS under vfs.usermount=1 with a single mount syscall + file writes; also feeds the unbounded-node variants of DF-3179. Fix: clamp pages to pages_limit for cred->cr_uid != 0.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3182 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
build.log β€” 65 B view raw
build.sh β€” 68 B view raw
env.txt β€” 419 B view raw
run.log β€” 1.1 KB view raw
run.sh β€” 91 B view raw
umount_demo.c β€” 3.3 KB view raw
verdict.json β€” 2.2 KB view raw

DF-3182 β€” tmpfs usermount memory cap is attacker-declared: size_max is never clamped to pages_limit

What

tmpfs_mount computes a system cap pages_limit = vm_swap_max + vmstats.v_page_count / 2 (sys/vfs/tmpfs/tmpfs_vfsops.c:157) but then applies it only to the default (size_max == 0) case, halved. For any explicit size_max the mount takes the attacker's value verbatim β€” the OFF_TO_IDX(size_max) > pages_limit branch is deliberately unclamped (comment at :164-169: fstab boot-order concerns), up to OFF_TO_IDX(INT64_MAX) = 2^51 pages:

    } else if (OFF_TO_IDX(size_max) > pages_limit) {
        /* do not force pages = pages_limit ... */
        pages = OFF_TO_IDX(size_max);      /* no clamp! */
    } else {
        pages = OFF_TO_IDX(size_max);      /* identical branch */
    }

There is no distinction between root mounts (where the fstab rationale may apply) and non-root usermounts: with vfs.usermount=1, any user mounting a tmpfs on a directory they own sets tm_pages_max (and the derived tm_nodes_max) arbitrarily (sys/kern/vfs_syscalls.c:151-155 β€” SYSCAP_NOMOUNT_TMPFS under __SYSCAP_NOROOTTEST, default creds are unrestricted). The ENOSPC machinery in tmpfs_reg_resize (tmpfs_subr.c:1007-1011) then compares against the attacker's number and never fires β€” the only backstop is real machine-wide memory exhaustion.

Reproduce (observed on the guest, unprivileged user maxx, vfs.usermount=1)

# ./umount_demo /home/maxx/dfm1
mounter uid=1001 euid=1001, spoofing ta_root_uid/gid=0, ta_size_max=1TiB
statfs: f_blocks=268435456 (x PAGE_SIZE = 1099511627776 bytes = 1024 GiB), ...

The designed cap on this guest (4 GB RAM, 4 GB swap) would be (1048576 + 520k)/2 β‰ˆ 784k pages β‰ˆ 3 GiB; the user-mounted tmpfs reports 268,435,456 pages (1 TiB), i.e. 341x past the intended bound. Writing past 3 GiB into such a mount succeeds (no ENOSPC) until the machine exhausts memory β€” unprivileged whole-system DoS (swap thrash/OOM), by design of the missing clamp.

Fix

For non-root mounters (cred->cr_uid != 0), clamp: if (pages > pages_limit) pages = pages_limit; (the boot-order rationale in the comment concerns root fstab mounts only). Optionally also enforce a global sum across user-mounted tmpfses.

VERDICT.md
↓ download raw

DF-3182 verdict β€” REPRODUCED (mount-side limit derivation accepts attacker-declared size for usermounts)

Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (INVARIANTS), 4.25 GB RAM, 4 GB swap, vfs.usermount=1 (set as root for the demo; it is the documented precondition for unprivileged mounts β€” default is 0).

As unprivileged user maxx (uid 1001), mounting a tmpfs on a self-owned directory with ta_size_max = 1 TiB:

statfs: f_blocks=268435456 (x PAGE_SIZE = 1099511627776 bytes = 1024 GiB)

The kernel-derived system cap for this machine would be pages_limit = vm_swap_max + v_page_count/2 = 1,568,895 pages, halved for defaults = 784,447 pages (~3 GiB). The user's mount stores tm_pages_max = 268,435,456 pages β€” 341x the intended bound β€” because tmpfs_vfsops.c:163-173 assigns pages = OFF_TO_IDX(size_max) in BOTH the over-limit and normal branches (the comment admits the clamp was dropped for boot-time fstab ordering; usermounts were not considered).

Consequence: the ENOSPC guard in tmpfs_reg_resize (tmpfs_subr.c:1007-1011) compares against the attacker's number and cannot fire; filling such a mount degrades only at whole-machine memory exhaustion (swap thrash/OOM) β€” an unprivileged system-wide DoS under vfs.usermount=1. The actual OOM wedge was not executed on the single-tenant guest to keep it usable for the other verifications; the bypass itself (f_blocks + successful writes into the mount beyond any kernel-derived cap) is captured in run.log.

Note: even without this bug, per-mount caps are per-mount (a user could stack several clamped mounts); the bug reduces the cost to one syscall and one mount and removes the kernel's own notion of a bound entirely. Rated Low-to-Medium accordingly (conditional on vfs.usermount=1).

Status: reproduced / impact=dos (limit bypass demonstrated; OOM consequence immediate) / confidence certain for the derivation, likely for the machine-wide impact.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean
[]
per-fix-DF-3182

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log (f_blocks=268435456 = 1TiB from uid 1001 mount)', 'umount_demo.c', 'VERDICT.md']

PoC changes

sys/statfs.h include removed (DF: statfs in sys/mount.h); struct tmpfs_mount_info mirrored locally

Verified recommended fix

clamp pages to pages_limit for non-root mounters (cred->cr_uid != 0)

Verdict

REPRODUCED (derivation bypass): with vfs.usermount=1, unprivileged user maxx (uid 1001) mounted a tmpfs with ta_size_max=1TiB; statfs reports f_blocks=268435456 pages (1024 GiB) vs the kernel-derived machine cap of ~784,447 pages (~3GiB) β€” tmpfs_vfsops.c:163-173 assigns pages=OFF_TO_IDX(size_max) with no clamp in the over-limit branch (identical code in both branches; the comment admits the clamp was dropped for root fstab boot-order reasons, and usermounts were not considered). tm_pages_max (and derived tm_nodes_max) are therefore attacker-declared; the ENOSPC guard in tmpfs_reg_resize (tmpfs_subr.c:1007) cannot fire and the only backstop is machine-wide memory exhaustion. The OOM wedge itself was not executed to preserve the single-tenant guest; the bypass is captured in run.log.