On Mon, Sep 14, 2026 at 7:46 PM Ren Wei <[email protected]> wrote:
>
> From: Zixuan Chai <[email protected]>
>
> 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: [email protected]
> Reported-by: VEGA <[email protected]>
> Assisted-by: LLM
> Signed-off-by: Zixuan Chai <[email protected]>
> Signed-off-by: Ren Wei <[email protected]>
> ---


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.

Reply via email to