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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2714 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- 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.
- 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 -9does 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).
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).
- 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. - Bounded run (run.log, kernel #0):
./sendfile_hdtr_dos 56as the unprivileged user maxx β netstat -m sampled at 100 ms showed26684/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). - Wedge run (run.2.log, kernel #0):
./sendfile_hdtr_dos 256β after 15 s:33494/33296 mbuf clusters in use, process stateD3(non-interruptible machine-descheduled) withWCHAN=objcache, sleeping in the objcache allocator for clusters that will never be freed because the wedged syscall itself holds them all.kill -9was delivered and had no effect (stillD3 objcache). The syscall never returned; the guest had to be reset. - 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)
- sys/kern/uipc_syscalls.c:1617-1642 (hdtr marshalling before any check)
- sys/kern/uipc_syscalls.c:1637 (m_uiomove on unbounded uio_resid)
- sys/kern/kern_subr.c:455-489 (iovec_copyin allows SSIZE_MAX totals)
- sys/kern/uipc_mbuf.c:2664-2691 (m_uiomove unbounded M_WAITOK loop)
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
fixedApplied 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']
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).
No comments yet.