Re: [PATCH RFC -next 0/5] net: charge socket memory budget to memcg upfront
From: Eric Dumazet
Date: Thu Sep 24 2026 - 05:56:41 EST
On Thu, Sep 24, 2026 at 11:25 AM Cai Xinchen <caixinchen1@xxxxxxxxxx> wrote:
>
> Hi,
>
> Thank you for the review.
>
> We found that sk_forward_alloc is a plain int updated by a non-atomic
> RMW (sk_forward_alloc_add(), where even the read side is not
> READ_ONCE), and its writers span three unrelated lock domains:
>
> - socket lock, process context: SO_RESERVE_MEM and TX grants
> (__sk_mem_schedule(), sk_forced_mem_schedule());
>
> - receive-queue lock, softirq: UDP RX charges and the
> udp_rmem_release() fold;
>
> - no lock at all: sk_mem_charge()/sk_mem_uncharge() from
> skb_set_owner_r() and skb destructors, and the sk_mem_reclaim()
> fold itself.
>
> Any cross-domain pair loses an update. A lost charge is still
> returned in full by the matching skb free, so it resurfaces as a
> phantom surplus in sk_forward_alloc, and the next fold hands it back
> to the memcg via __sk_mem_reduce_allocated() ->
> mem_cgroup_sk_uncharge() - an uncharge with no matching charge.
>
> We first tried to fix it with locks, and hit three walls:
>
> - the socket lock cannot be used: its holders free skbs (e.g.
> tcp_recvmsg()), and the destructor's sk_mem_uncharge() would
> need to re-acquire it - recursion. That is why these helpers
> are lockless in the first place;
>
> - a new per-socket spinlock serializes the RMWs but not the bug:
> __sk_mem_schedule() publishes the grant before the memcg charge,
> and the charge may sleep (GFP_KERNEL, memcg reclaim/OOM), so no
> spinlock can cover both steps; a fold in that window can still
> refund pages whose charge afterwards fails;
>
> - such a lock would also sit on the per-packet charge/uncharge
> paths, exactly the hot path the cacheline layout around
> sk_forward_alloc was tuned to keep cheap.
>
> Are there any good solutions to solve this problem?
Perfect, you now gave us what we need.
UDP is broken, it should be easy to fix without breaking TCP.
I am surprised your LLM went to a completelly broken path.
sk_forward_alloc is never supposed to be updated locklessly across
multiple lock domains:
- For TCP, sk_forward_alloc is strictly serialized by the socket lock
(lock_sock / bh_lock_sock). TCP does not use sock_rfree() as an skb
destructor (sk_mem_uncharge() is called under the socket lock in
tcp_eat_recv_skb() and sk_wmem_free_skb(), while TX destructors
sock_wfree() / tcp_wfree() only touch sk_wmem_alloc).
- For UDP, sk_forward_alloc is serialized by sk->sk_receive_queue.lock
(in __udp_enqueue_schedule_skb() and udp_rmem_release()).
Your reproducer and analysis point to two specific places that violate
these locking rules:
1. SO_RESERVE_MEM on UDP sockets:
SO_RESERVE_MEM (commit 2bb2f5fb21b0, "net: add new socket option
SO_RESERVE_MEM") was designed for TCP, where sk_forward_alloc is
protected by lock_sock(sk) and sk_mem_reclaim() checks
sk_unused_reserved_mem(sk).
However, sock_reserve_memory() only checks sk_has_account(sk), which
also matches UDP. UDP does not support SO_RESERVE_MEM:
udp_rmem_release() does not check sk_unused_reserved_mem(sk) (so the
first recv() reclaims the reserved pages from sk_forward_alloc), and
setsockopt(SO_RESERVE_MEM) only holds lock_sock(sk) instead of
sk->sk_receive_queue.lock, racing with __udp_enqueue_schedule_skb()
and udp_rmem_release().
We can fix this directly in sock_reserve_memory():
diff --git a/net/core/sock.c b/net/core/sock.c
index 1d5927cd49a1..763c2017d9ef 100644
--- a/net/core/sock.c
+++ b/net/core/sock.c
@@ -1034,7 +1034,7 @@ static int sock_reserve_memory(struct sock *sk, int bytes)
bool charged;
int pages;
- if (!mem_cgroup_sk_enabled(sk) || !sk_has_account(sk))
+ if (!mem_cgroup_sk_enabled(sk) || !sk_is_tcp(sk))
return -EOPNOTSUPP;
2. BPF sockmap (net/core/skmsg.c):
As you noted in the reproducer comments, sk_psock_skb_ingress() and
sk_psock_skb_ingress_self() call sk_rmem_schedule() and
skb_set_owner_r() (which installs sock_rfree() as skb->destructor)
from sk_psock_backlog() or after dropping sk_receive_queue.lock in
udp_read_skb(). Calling skb_set_owner_r() / sock_rfree() without the
socket lock (or sk_receive_queue.lock for UDP) on protocols with
sk_has_account(sk) is a bug in net/core/skmsg.c and should be fixed
there.