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

kern_dup() dup2/dup3 onto an in-flight reserved descriptor: unbounded uninterruptible retry loop + unrate-limited kprintf console/msgbuf flood

Summary

dup2 onto a descriptor a sibling thread holds reserved (pipe's fd1 window, kqueue, SCM_RIGHTS) parks 1s in a NON-PCATCH tsleep with an unrate-limited kprintf per iteration: signal-immune for the full tick per iteration, unkillable for as long as a blocked reservation holder waits (M_WAITOK under pressure), and K threads -> K lines/sec console flood (observed overwriting a panic message on serial). Unpriv two-thread rig: 10 warnings + 10 full-1s parks, SIGKILL death ~1s; krate+PCATCH fix validated (<=1 line/sec system-wide, EINTR on signal, immediate death).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5793 Β· 14 files
FileTypeDescriptionSize
dup_directed.c β€” 1.7 KB view raw
dup_reserved.c β€” 2.7 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 157 B view raw
build.log β€” 331 B view raw
run.log β€” 807 B view raw
run.2.log β€” 336 B view raw
run.3.log β€” 636 B view raw
fixrun.log β€” 808 B view raw
env.txt β€” 413 B view raw
VERDICT.md β€” 4.0 KB ↓ raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.3 KB view raw
VERDICT.md
↓ download raw

DF-5793 VERDICT β€” reproduced (dos: unkillable spin + console/msgbuf flood); fix validated

Bottom line

REPRODUCED. On the stock DragonFly 6.5-DEVELOPMENT guest, an unprivileged two-thread process triggered the kern_dup() reserved-target retry path at ~8.5% of dup2() attempts; every hit (a) emitted an unrate-limited Warning: dup(): target descriptor %d is reserved line to the console/dmesg and (b) parked the dup2 thread for a full 1-second uninterruptible tsleep, with the retry loop unbounded while the peer thread keeps re-reserving descriptors. kill -9 delivered mid-park did not take effect for ~1s (measured). The kernel-message flood was even observed overwriting a kernel panic message on the serial console during DF-5792 verification (serial_panic_run1.txt). Severity: Low (local DoS / log flood; the unkillable-window becomes permanent if the reservation holder blocks indefinitely, e.g. M_WAITOK kmalloc under memory pressure).

Root cause (path:line)

Evidence (stock kernel #0, unprivileged user maxx uid 1001)

run.log (directed, 10s):

spammer: attempts=117 slow=10 err=0 errno_last=9(Bad file descriptor)
directed: pipes=5129795
dmesg reserved-warnings: 10        <- exactly one per 1s park
Warning: dup(): target descriptor 3 is reserved, waiting for it to be resolved   (x10)

run2.log (sweep variant, 12s):

DF-5793: pipes=4525216 dup2 attempts=16184 slow(>=250ms dup2)=11 err=0
total reserved-warnings now: 22    <- 12 new

kill.log (SIGKILL latency, stock):

pid=1315
SIGKILL-to-death sec: 1           <- uninterruptible park survives SIGKILL ~1s
warnings: 32

Note: only 117 dup2 attempts completed in 10s β€” each of the 10 hits consumed a full second parked; steady state is one warning line + one parked second per hit, per thread, forever.

Fix validation (kernel #2 Mon Oct 12 19:39:15 UTC 2026)

fix.diff: krateprintf(&krate_dupres /* .freq = 1 */, ...) and tsleep(fdp, PCATCH, "fdres", hz) β†’ return (EINTR) on signal.

fixrun.log:

  • patched directed run: errno_last=4 (Interrupted system call) β€” the once-uninterruptible wait now returns EINTR;
  • 4 concurrent rigs Γ— 10s β†’ 10 warning lines total (krate cap; the unpatched per-thread rate of 10 lines/10s would give ~40);
  • SIGKILL mid-park β†’ death in 0s (vs 1s stock).

fix_status: fixed (flood bounded to ≀1 line/sec system-wide; interruptible; no behavior change for the non-racing dup2 path β€” the DF-5792 storms ran 20.8M opens/closes plus dup traffic on the same patched kernel with zero anomalies).

Rig notes (what had to be fixed vs a naive PoC)

  • dup2() returns the new fd on success (not 0) β€” a == 0 success check silently miscounts;
  • the dup2 source fd must live outside the contested range (e.g. dup2'd to fd 250) or the churner's close-storm closes it and every dup2 returns EBADF;
  • ssh sessions may run with fd 0 closed β€” never use fd 0 as the source.

Environment

Same guest as DF-5792 (see ../DF-5792/env.txt): DragonFly 6.5-DEVELOPMENT

0 baseline; patched #2 for fix validation. Guest reset afterwards.

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: unbounded 1 line/sec/thread + 1s uninterruptible parks (SIGKILL deferred). Patched: <=1 line/sec system-wide (4 rigs -> 10 lines/10s vs ~40 extrapolated), dup2 returns EINTR when signalled, SIGKILL death 0s.

fixrun.log sections a/b/c; dmesg counts in run.log/run.2.log/fixrun.log.
↓ fix.diffper-fix-DF-5793

Confirmed kernel references

Detail

Exploit chain

thread A: pipe() churn keeps the predictable lowest-free fd (3) transiently reserved (~8.5% of wall time measured); thread B: dup2(srcfd=250, 3) -> kern_dup reserved branch -> kprintf + tsleep(fdp,0,'fdres',hz) + goto retry, no bound, no PCATCH. K threads -> K msgbuf/console lines per second and K unkillable-parked threads.

Evidence (decisive lines)

run.log (attempts=117 slow=10, 10 dmesg warnings), run.2.log (sweep: 16184 attempts, 11 slow, +12 lines), run.3.log (SIGKILL-to-death 1s), fixrun.log (patched: EINTR observed, 4 rigs/10s -> 10 lines total vs ~40 expected, SIGKILL-to-death 0s).

PoC changes

Rigs written fresh. Bring-up fixes: dup2() success check must be >=0 (returns new fd, not 0); the dup2 source fd must be moved out of the contested lowest-free range (fd 250) or the pipe churner closes it (every dup2 EBADF); never use fd 0 as source over ssh (stdin closed).

Verified recommended fix

krateprintf the warning and make the tsleep PCATCH returning EINTR (see fix.diff).

Verdict

Unprivileged two-thread process hits kern_dup()'s reserved-target path (kern_descrip.c:1143-1151): pipe() holds the read-side fd reserved across its second falloc/fsetfd (sys_pipe.c:290-338), and a sibling thread's dup2() onto that fd takes an unbounded retry loop of 1-second NON-PCATCH tsleeps, each printing an unrate-limited kernel warning. Measured on stock guest: 10/117 dup2 attempts parked the full 1s and printed 10 dmesg lines in 10s (1 line/sec/thread sustained, scales per thread); SIGKILL delivered mid-park took ~1s to kill the process (uninterruptible); the same flood was observed overwriting a kernel panic message on the serial console during DF-5792 verification. With a persistent reservation (memory-pressure-blocked kmalloc, SCM_RIGHTS receive loop) the dup2 thread is unkillable indefinitely.