Subject: TCP receive memory accounting before accept() with cgroup v2
flat view
From: Cyclinder Kuo <hidden>
Date: 2026-09-15 09:08:07
Also in:
cgroups, linux-mm
Hi,
"Resending in plain text; the previous message was rejected by netdev
because it contained an HTML part."
While looking into whether Kubernetes can allow net.core.somaxconn
by default on cgroup v2 nodes [1], I tested how receive memory is
accounted for connections waiting in the accept queue.
I understand that d752a4986532 ("net: memcg: late association of sock
to memcg") deliberately moved memcg association to accept() time.
The discussion also explicitly covered data queued before accept()
and charging that memory when the socket is accepted [2].
My question is about memory containment before that happens:
if an application delays accept() indefinitely, how should the
receive memory of those pending connections be constrained by the
service's memcg?
Test setup
----------
- Ubuntu 24.04, kernel 6.8.0-53-generic
- Kubernetes v1.35.0, cgroup v2
- Server container and Pod cgroups: memory.max = 512 MiB
- Private network namespace, somaxconn = 65535
- Server calls listen(65535), initially without calling accept()
- Client runs on a different node
The server container was privileged to set somaxconn in its own
network namespace. The host's somaxconn remained unchanged.
The client established 256 TCP connections, then sent 64 KiB on
each connection, for a total of 16 MiB. The server subsequently
accepted all 256 connections without reading any data.
Results
-------
Phase Pending accepts Total TCP
Container memcg
accepts
Recv-Q memory.current sock
Baseline 0
0 8.49 MiB 0
Connected, no payload 256 0
8.49 MiB 0
Payload, before accept 256 16 MiB
8.49 MiB 0
After accept, no reads 0 16 MiB
33.82 MiB 25 MiB
I used ss in the server's network namespace to observe the pending
accept count and the established sockets' receive queues. I also
read memory.current and memory.stat in both the container and its
parent Pod cgroup. Both reported sock=0 before accept(), and
25 MiB after accept().
The data was already queued on the server, but its receive memory
was only charged to the server's memcg when accept() was called.
I did not exceed memory.max or test memory exhaustion, so this
does not demonstrate a node OOM or an absence of other network
memory limits.
Current implementation
----------------------
I also inspected upstream v7.2, although I have not repeated the
runtime test on that kernel.
sk_clone() still sets newsk->sk_memcg to NULL, with the comment
"sk->sk_memcg will be populated at accept() time" [3].
__inet_accept() then calls mem_cgroup_sk_alloc() and
__sk_charge() [4].
I also found the discussion explaining why simply inheriting the
listener's sk_memcg is insufficient, particularly when a listener
is created in the root cgroup and passed to a process in another
cgroup [5].
Questions
---------
Is the pre-accept accounting gap an intentional limitation of
memcg socket accounting?
Has temporarily charging pending connections to the listener's
memcg, then reconciling ownership at accept(), been considered?
I realize this would need to handle different listener/accepter
cgroups and the root-cgroup listener case.
If that is not a suitable approach, is there another mechanism
that should bound this memory on a per-service or per-memcg basis?
For the Kubernetes change, we are holding off on treating
somaxconn as safe based solely on cgroup v2 until we understand
this better. Pointers to previous discussions would be appreciated.
[1] https://github.com/kubernetes/kubernetes/issues/134194
[2] https://lore.kernel.org/all/20200222010456.40635-1-shakeelb@google.com/T/ (local)
[3] https://github.com/torvalds/linux/blob/v7.2/net/core/sock.c#L2542-L2547
[4] https://github.com/torvalds/linux/blob/v7.2/net/ipv4/af_inet.c#L761-L766
[5] https://lore.kernel.org/all/CAAVpQUDUULCrcTP4AQ31B5bfo-+dtw3H8CQGq9_SQ7d28xXSvA@mail.gmail.com/ (local)
Thanks,
Qifeng Guo