* Re: [PATCH net] tcp: tcp_metrics: use the socket netns, not the dst device netns
2026-10-16 8:48 [PATCH net] tcp: tcp_metrics: use the socket netns, not the dst device netns Eric Dumazet
@ 2026-10-16 8:53 ` netdev-bot+sinfo
2026-10-16 9:11 ` Eric Dumazet
2026-10-16 15:55 ` Neal Cardwell
1 sibling, 1 reply; 5+ messages in thread
From: netdev-bot+sinfo @ 2026-10-16 8:53 UTC (permalink / raw)
To: Eric Dumazet
Cc: David S . Miller, Jakub Kicinski, Paolo Abeni, Neal Cardwell,
Kuniyuki Iwashima, Simon Horman, netdev, Qiliang Yuan
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
- Whether the issue was actually triggered, or is only theoretical
(e.g. found by code inspection). If it was triggered please include
the symptoms, like the stack trace or error messages.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net] tcp: tcp_metrics: use the socket netns, not the dst device netns
2026-10-16 8:48 [PATCH net] tcp: tcp_metrics: use the socket netns, not the dst device netns Eric Dumazet
2026-10-16 8:53 ` netdev-bot+sinfo
@ 2026-10-16 15:55 ` Neal Cardwell
2026-10-16 16:08 ` Eric Dumazet
1 sibling, 1 reply; 5+ messages in thread
From: Neal Cardwell @ 2026-10-16 15:55 UTC (permalink / raw)
To: Eric Dumazet
Cc: David S . Miller, Jakub Kicinski, Paolo Abeni, Kuniyuki Iwashima,
Simon Horman, netdev, Qiliang Yuan
[-- Attachment #1: Type: text/plain, Size: 2375 bytes --]
On Fri, Oct 16, 2026 at 4:48 AM Eric Dumazet <edumazet@kernel.org> wrote:
>
> tcp_get_metrics(), __tcp_get_metrics_req() and tcpm_new() look up and
> create metrics entries in the netns of the cached dst device rather than
> in the netns of the socket.
>
> These usually match, but not after the device has been moved to another
> netns or unregistered: a socket can keep a stale dst in sk_dst_cache
> until it is checked again, and tcp_update_metrics() reads it with
> __sk_dst_get() without any dst_check(). Once dst_dev_put() has run, the
> dst device is blackhole_netdev and the entry is created in init_net.
> Before that, for instance for routes through nexthop objects whose
> cached dsts are released from a nested RCU callback, it is the netns the
> device has just been moved to.
>
> A TCP socket of netns A that enters TIME_WAIT in that window then
> creates or updates an entry carrying A's addresses and RTT in another
> netns, where TCP_METRICS_CMD_GET can dump it and where new connections
> to the same peer inherit those metrics.
>
> The metrics belong to the netns of the socket, so use sock_net() for
> full sockets and request sockets, and pass it to tcpm_new().
>
> Kernel sockets that do not hold a netns reference can still reach
> tcp_time_wait() after tcp_net_metrics_exit_batch() has flushed the
> entries of their dismantling netns, and an entry created then would
> never be flushed and would keep a dangling tcpm_net. Make tcpm_new()
> refuse to create entries when check_net() fails. Its callers are in
> an RCU read-side section, and cleanup_net() waits for a grace period
> between the refcount reaching zero and the flush, so an entry added
> after a successful check is always seen by the flush.
>
> Fixes: 51c5d0c4b169 ("tcp: Maintain dynamic metrics in local cache.")
> Signed-off-by: Eric Dumazet <edumazet@kernel.org>
> Cc: Qiliang Yuan <odys.yuan@gmail.com>
> ---
Reviewed-by: Neal Cardwell <ncardwell@google.com>
Tested-by: Neal Cardwell <ncardwell@google.com>
Thanks, Eric!
I got an LLM to write and run a repro that verifies the bug and
validates the fix. The script and its outputs are attached. In the
control case, both kernels put the entry in netns A. In the three
trigger cases, the base kernel puts the entry in init_net and the
patched kernel puts it in netns A.
neal
[-- Attachment #2: output.txt --]
[-- Type: text/plain, Size: 4319 bytes --]
Output:
# Base: unpatched net/main (191aa41548b2)
) time vng --run /tmp/bzImage.base --disable-kvm --cpus 2 --memory 2G --user root --exec "python3 $REPRO"
kernel: 7.3.0-rc7+
=== v4 none (control): "true" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : 10.9.0.2 age 0.470sec cwnd 10 rtt 5900us rttvar 11200us source 10.9.0.1
netns B : (none)
=== v4 move-dev-to-netns-B: "ip -n A link set vA netns B" (client state before close: FIN-WAIT-2)
init_net : 10.9.0.2 age 0.384sec cwnd 10 rtt 5164us rttvar 10188us source 10.9.0.1
netns A (socket) : (none)
netns B : (none)
=== v4 delete-dev: "ip -n A link del vA" (client state before close: FIN-WAIT-2)
init_net : 10.9.0.2 age 0.379sec cwnd 10 rtt 5164us rttvar 10161us source 10.9.0.1
netns A (socket) : (none)
netns B : (none)
=== v4 delete-addr: "ip -n A addr del 10.9.0.1/24 dev vA" (client state before close: FIN-WAIT-2)
init_net : 10.9.0.2 age 0.379sec cwnd 10 rtt 5207us rttvar 10268us source 10.9.0.1
netns A (socket) : (none)
netns B : (none)
=== v6 none (control): "true" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : fd00:9::2 age 0.457sec cwnd 10 rtt 69289us rttvar 113120us source fd00:9::1
netns B : (none)
=== v6 move-dev-to-netns-B: "ip -n A link set vA netns B" (client state before close: FIN-WAIT-2)
init_net : fd00:9::2 age 0.382sec cwnd 10 rtt 69134us rttvar 112948us source fd00:9::1
netns A (socket) : (none)
netns B : (none)
=== v6 delete-dev: "ip -n A link del vA" (client state before close: FIN-WAIT-2)
init_net : fd00:9::2 age 0.379sec cwnd 10 rtt 66492us rttvar 107579us source fd00:9::1
netns A (socket) : (none)
netns B : (none)
=== v6 delete-addr: "ip -n A -6 addr del fd00:9::1/64 dev vA" (client state before close: FIN-WAIT-2)
init_net : fd00:9::2 age 0.376sec cwnd 10 rtt 69172us rttvar 112945us source fd00:9::1
netns A (socket) : (none)
netns B : (none)
real 1m12.862s
user 0m56.941s
sys 0m19.470s
# Patched: net/main + Eric's patch
) time vng --run /tmp/bzImage.patched --disable-kvm --cpus 2 --memory 2G --user root --exec "python3 $REPRO" ; xterm
kernel: 7.3.0-rc7+
=== v4 none (control): "true" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : 10.9.0.2 age 0.477sec cwnd 10 rtt 5895us rttvar 11174us source 10.9.0.1
netns B : (none)
=== v4 move-dev-to-netns-B: "ip -n A link set vA netns B" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : 10.9.0.2 age 0.489sec cwnd 10 rtt 5153us rttvar 10152us source 10.9.0.1
netns B : (none)
=== v4 delete-dev: "ip -n A link del vA" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : 10.9.0.2 age 0.486sec cwnd 10 rtt 5147us rttvar 10150us source 10.9.0.1
netns B : (none)
=== v4 delete-addr: "ip -n A addr del 10.9.0.1/24 dev vA" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : 10.9.0.2 age 0.499sec cwnd 10 rtt 5148us rttvar 10144us source 10.9.0.1
netns B : (none)
=== v6 none (control): "true" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : fd00:9::2 age 0.490sec cwnd 10 rtt 67433us rttvar 109274us source fd00:9::1
netns B : (none)
=== v6 move-dev-to-netns-B: "ip -n A link set vA netns B" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : fd00:9::2 age 0.486sec cwnd 10 rtt 68010us rttvar 110627us source fd00:9::1
netns B : (none)
=== v6 delete-dev: "ip -n A link del vA" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : fd00:9::2 age 0.498sec cwnd 10 rtt 67152us rttvar 109100us source fd00:9::1
netns B : (none)
=== v6 delete-addr: "ip -n A -6 addr del fd00:9::1/64 dev vA" (client state before close: FIN-WAIT-2)
init_net : (none)
netns A (socket) : fd00:9::2 age 0.476sec cwnd 10 rtt 69384us rttvar 113220us source fd00:9::1
netns B : (none)
real 1m17.848s
user 0m53.937s
sys 0m30.023s
[-- Attachment #3: tcpm_netns_repro.py --]
[-- Type: text/x-python, Size: 4152 bytes --]
#!/usr/bin/env python3
# Reproducer for: "tcp: tcp_metrics: use the socket netns, not the dst device netns"
#
# A TCP client socket in netns A goes to FIN_WAIT2 (shutdown(SHUT_WR), FIN ACKed),
# then its route/device goes away (device moved to netns B, device deleted, or
# address/route deleted) while the socket keeps the now-stale dst in
# sk_dst_cache. close() then runs tcp_time_wait() -> tcp_update_metrics()
# synchronously, which uses __sk_dst_get() without dst_check().
#
# We then report in which netns the tcp_metrics entry for the peer shows up.
# Expected without the patch: in init_net (dst->dev == blackhole_netdev).
# Expected with the patch: in netns A (the socket's netns).
import os
import socket
import subprocess
import sys
import time
ROOT_NS = os.open('/proc/self/ns/net', os.O_RDONLY)
def sh(cmd, check=True):
r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if check and r.returncode:
print(f'CMD FAILED: {cmd}\n{r.stdout}{r.stderr}', flush=True)
sys.exit(1)
return r.stdout
def enter_ns(name):
fd = os.open(f'/run/netns/{name}', os.O_RDONLY)
os.setns(fd, os.CLONE_NEWNET)
os.close(fd)
def enter_root():
os.setns(ROOT_NS, os.CLONE_NEWNET)
def metrics(ns):
pfx = f'ip -n {ns} ' if ns else 'ip '
out = sh(pfx + 'tcp_metrics show', check=False).strip()
return out if out else '(none)'
def recv_exact(s, n):
buf = b''
while len(buf) < n:
chunk = s.recv(n - len(buf))
if not chunk:
raise RuntimeError('unexpected EOF')
buf += chunk
return buf
def setup():
for ns in ('A', 'B', 'S'):
sh(f'ip netns del {ns}', check=False)
sh('ip tcp_metrics flush all', check=False)
for ns in ('A', 'B', 'S'):
sh(f'ip netns add {ns}')
sh(f'ip -n {ns} link set lo up')
sh('ip link add vA netns A type veth peer name vS netns S')
sh('ip -n A addr add 10.9.0.1/24 dev vA')
sh('ip -n S addr add 10.9.0.2/24 dev vS')
sh('ip -n A -6 addr add fd00:9::1/64 dev vA nodad')
sh('ip -n S -6 addr add fd00:9::2/64 dev vS nodad')
sh('ip -n A link set vA up')
sh('ip -n S link set vS up')
time.sleep(0.3)
TRIGGERS = {
'none (control)': 'true',
'move-dev-to-netns-B': 'ip -n A link set vA netns B',
'delete-dev': 'ip -n A link del vA',
'delete-addr': None, # filled per family
}
def run_case(family, trigger):
setup()
if family == socket.AF_INET:
peer, fam = '10.9.0.2', 'v4'
addrdel = 'ip -n A addr del 10.9.0.1/24 dev vA'
else:
peer, fam = 'fd00:9::2', 'v6'
addrdel = 'ip -n A -6 addr del fd00:9::1/64 dev vA'
cmd = TRIGGERS[trigger] or addrdel
enter_ns('S')
srv = socket.socket(family, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((peer, 5000))
srv.listen(1)
enter_ns('A')
cli = socket.socket(family, socket.SOCK_STREAM)
enter_root()
cli.connect((peer, 5000))
conn, _ = srv.accept()
for _ in range(20): # get an RTT sample
cli.sendall(b'x' * 1000)
recv_exact(conn, 1000)
conn.sendall(b'y' * 1000)
recv_exact(cli, 1000)
cli.shutdown(socket.SHUT_WR) # FIN -> ACKed -> FIN_WAIT2
time.sleep(0.5)
state = [ln.split()[0] for ln in
sh('ip netns exec A ss -Htan', check=False).splitlines()
if ':5000' in ln]
sh(cmd)
time.sleep(1.0) # let RCU callbacks (dst_dev_put()) run
cli.close() # FIN_WAIT2 -> tcp_time_wait() -> tcp_update_metrics()
time.sleep(0.3)
print(f'=== {fam} {trigger}: "{cmd}" (client state before close: '
f'{state[0] if state else "?"})', flush=True)
for ns, label in ((None, 'init_net'), ('A', 'netns A (socket)'),
('B', 'netns B')):
print(f' {label:18s}: {metrics(ns)}', flush=True)
conn.close()
srv.close()
def main():
print('kernel:', os.uname().release, flush=True)
for family in (socket.AF_INET, socket.AF_INET6):
for trigger in TRIGGERS:
run_case(family, trigger)
if __name__ == '__main__':
main()
^ permalink raw reply [flat|nested] 5+ messages in thread