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

sendfile(2) marshals hdtr headers into an unbounded M_WAITOK mbuf chain before any socket validation β€” attacker-chosen kernel memory exhaustion and uninterruptible full mbuf-pool wedge (unkillable, reboot required)

Field Value
ID DF-2714
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-770 Allocation of Resources Without Limits
File sys/kern/uipc_syscalls.c
Lines 1617-1654 (enabling primitive uipc_mbuf.c:2664-2691)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

sys_sendfile() runs iovec_copyin() on the hdtr header iovec (totals allowed up to SSIZE_MAX-1) and then m_uiomove(), which allocates mbuf clusters with M_WAITOK in an unbounded loop β€” all before kern_sendfile() validates the socket or accounts against any socket buffer. Unlike sendmsg/sosend (which self-throttles on ssb_space), there is no bound at all.

Threat model & preconditions

Any unprivileged local user with any readable file fd and any socket fd. 56MB header: kernel transiently holds 80% of the global mbuf cluster pool during one syscall. Header larger than the pool: the syscall sleeps uninterruptibly in the objcache allocator (D3, WCHAN=objcache) holding essentially every cluster; kill -9 has no effect; system-wide network RX/TX starves until reboot.

Proof of contest

VERIFIED (findings/poc/DF-2714/sendfile_hdtr_dos.c): ./sendfile_hdtr_dos 56 shows the 26.7K/33.3K cluster spike then EINVAL; ./sendfile_hdtr_dos 256 shows 33494/33296 clusters, D3/objcache, survives kill -9, never returns (guest reset required β€” performed). Fix (1MB SF_HDTR_MAXBYTES cap before m_uiomove) validated on kernel #2: EINVAL instantly, zero cluster spike.

Cap header and trailer totals immediately after each iovec_copyin, before m_uiomove (#define SF_HDTR_MAXBYTES (1024*1024); full diff in findings/poc/DF-2714/).

Timeline

  • 2026-08-30 Discovered during pass-2 audit of uipc_syscalls.c (GLM 5.3); unpriv pool-wedge reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2714 Β· 12 files
FileTypeDescriptionSize
sendfile_hdtr_dos.c β€” 2.1 KB view raw
build.sh β€” 58 B view raw
run.sh β€” 393 B view raw
build.log β€” 9 B view raw
run.log β€” 275 B view raw
run.2.log β€” 653 B view raw
fix_run.log β€” 290 B view raw
env.txt β€” 250 B view raw
fix.diff β€” 1.1 KB view raw
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
verdict.json β€” 3.6 KB view raw

DF-2714 β€” sendfile(2) hdtr headers: unbounded kernel mbuf allocation

What

sys_sendfile() (sys/kern/uipc_syscalls.c:1617-1642) marshals the sf_hdtr header iovec into an mbuf chain with m_uiomove() before kern_sendfile() performs any validation or socket-buffer accounting. iovec_copyin() only rejects overflow, so the header total may be up to SSIZE_MAX-1 bytes. Every iteration of m_uiomove() allocates mbuf clusters with M_WAITOK (sys/kern/uipc_mbuf.c:2664), bounded only by the global mbuf cluster pool / system memory.

Unlike sendmsg() (whose sosend() self-throttles on ssb_space()), the sendfile header path has no bound at all.

Threat

Any unprivileged local user: fd = any readable file (/etc/passwd), s = any socket (it need not even be valid for sendfile β€” validation happens after marshalling). Two consequences, both reproduced:

  1. Transient: an attacker-chosen volume of kernel mbuf memory (e.g. 56 MB = 80% of the guest's 33,296-cluster pool) is allocated and held for the duration of the copy.
  2. Persistent wedge: a request larger than the pool leaves the syscall sleeping uninterruptibly in the objcache allocator (state D3, WCHAN=objcache) while holding every mbuf cluster in the system. kill -9 does not work. Network RX/TX starves system-wide; reboot required.

Reproduce

cc -O2 -o sendfile_hdtr_dos sendfile_hdtr_dos.c
# bounded (watch netstat -m "mbuf clusters in use" during the call):
./sendfile_hdtr_dos 56
# wedge (request > pool; run under a shell you can afford to lose):
./sendfile_hdtr_dos 256

Baseline observations (kernel #0, unpatched): - run.log: peak 26684/33296 mbuf clusters in use during a 56 MB header sendfile that then returns EINVAL. - run.2.log: 256 MB request -> 33494/33296 mbuf clusters in use, process D3 objcache, survives kill -9, syscall never returns.

Patched (fix.diff, kernel #2): both runs return EINVAL immediately; peak cluster sample 256/33296 (idle baseline). fix_run.log.

Fix

Cap header/trailer totals at 1 MB (SF_HDTR_MAXBYTES) before marshalling β€” see fix.diff (validated).

VERDICT.md
↓ download raw

DF-2714 β€” VERDICT

Status: REPRODUCED (impact: dos β€” unprivileged, system-wide, reboot-required)

Narrative

Guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, 6 CPU, 4 GB RAM, kern.ipc.nmbclusters=33296 (~65 MB of 2 KB clusters).

  1. Code path confirmed: sys_sendfile() copies in the sf_hdtr, runs iovec_copyin() on hdtr.headers (sys/kern/uipc_syscalls.c:1625-1628), then m_uiomove(&auio) (line 1637) builds the ENTIRE header mbuf chain with M_WAITOK allocations before kern_sendfile() (line 1645) performs the first validation of the socket (SOCK_STREAM? connected? SSB_PREALLOC?). iovec_copyin() (sys/kern/kern_subr.c:448-494) permits totals up to SSIZE_MAX-1. m_uiomove() (sys/kern/uipc_mbuf.c:2664) loops while (uio_resid > 0) allocating a cluster per iteration. No bound anywhere.
  2. Bounded run (run.log, kernel #0): ./sendfile_hdtr_dos 56 as the unprivileged user maxx β€” netstat -m sampled at 100 ms showed 26684/33296 mbuf clusters in use (80% of the global pool; baseline 256) during the call; the call then returned EINVAL (socket is a plain UDP socket β€” proof that marshalling precedes validation).
  3. Wedge run (run.2.log, kernel #0): ./sendfile_hdtr_dos 256 β€” after 15 s: 33494/33296 mbuf clusters in use, process state D3 (non-interruptible machine-descheduled) with WCHAN=objcache, sleeping in the objcache allocator for clusters that will never be freed because the wedged syscall itself holds them all. kill -9 was delivered and had no effect (still D3 objcache). The syscall never returned; the guest had to be reset.
  4. Contrast with sendmsg(): sosend() checks ssb_space()/ssb_lowat before each mbuf append (sys/kern/uipc_socket.c:1916-1935 region) so a single sendmsg cannot hold more than the socket's send-buffer worth of mbufs; the sendfile header path bypasses all of that.

Root cause (path:line)

Fix validation

fix.diff adds SF_HDTR_MAXBYTES (1 MB) caps on header and trailer totals right after each iovec_copyin(). Built and installed as kernel #2 (alongside the DF-2713 fix, independent file); re-running the exact PoC: 56 MB and 256 MB requests both return EINVAL immediately, and the peak cluster sample during both runs was 256/33296 β€” the idle baseline (fix_run.log). Baseline wedge cannot be reproduced on the patched kernel. fix_status: fixed.

Note: on both baseline runs the failing sendfile copied out an uninitialized stack sbytes (printed sbytes=1) β€” that is the already filed DF-2695, not re-reported here.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff (SF_HDTR_MAXBYTES caps) to the guest's /usr/src, make nativekernel + installkernel (kernel #2, also carrying the DF-2713 fix - independent file), rebooted, re-ran the identical PoC at 56MB and 256MB: both return EINVAL immediately and the 100ms cluster sampler recorded a peak of 256/33296 (idle baseline). No allocation spike, no wedge.

['fix_run.log', 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Mon Aug 31 04:55:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

open("/etc/passwd") for fd; socket(AF_INET,SOCK_DGRAM) for s (any socket works - validation happens after marshalling); sf_hdtr with one iovec of attacker-chosen length; sendfile() allocates attacker-chosen mbuf memory. Below pool size: transient 80%+ pool hold. Above pool size: uninterruptible sleep holding the entire cluster pool -> system-wide network starvation until reboot.

Evidence (decisive lines)

["run.log: '26684/33296 mbuf clusters in use' sampled at 100ms during the 56MB run (baseline 256), call then returns EINVAL", "run.2.log: '33494/33296 mbuf clusters in use', process 'D3 objcache', survives kill -9, SYSCALL_RETURNED never printed", "fix_run.log: kernel #2 with fix.diff - 56MB and 256MB both EINVAL instantly, PEAK_CLUSTER_SAMPLE '256/33296'"]

PoC changes

Initial seed had no struct sf_hdtr (taken from sys/socket.h), missing errno.h; header size parameterized so the same binary performs both the bounded and the wedge variant.

Verified recommended fix

Cap sendfile hdtr header and trailer totals (SF_HDTR_MAXBYTES = 1MB) immediately after iovec_copyin, before m_uiomove (uipc_syscalls.c:1629/1659).

Verdict

sys_sendfile() marshals hdtr headers into an unbounded M_WAITOK mbuf chain (m_uiomove) before any socket validation or buffer accounting; reproduced as unprivileged user maxx on the stock kernel: a 56 MB header request transiently held 26684/33296 (80%) of the global mbuf cluster pool, and a 256 MB request left the syscall permanently blocked in the objcache allocator (D3, WCHAN=objcache) holding essentially every cluster, unkillable with kill -9, requiring a guest reset. A 1 MB cap on header/trailer totals (fix.diff, kernel #2) removes both the spike and the wedge (peak cluster sample 256/33296 = idle baseline).