Thread (7 messages) 7 messages, 3 authors, 2026-07-31

Re: [PATCH net 0/2] tcp: diag: fix unbounded bucket lock hold in diag dump paths

flat view

From: Jiayuan Chen <jiayuan.chen@linux.dev>
Date: 2026-07-29 13:55:55
Also in: mptcp

On 7/29/26 7:28 PM, Zihan Xi wrote:
Hi Linux kernel maintainers,

We found and validated a issue in net/ipv4/tcp_diag.c and
net/mptcp/mptcp_diag.c. The bug is reachable by a
non-root user via user and net namespace.
We've tested it, and it should not affect any other functionality.

We will provide detailed information about the bug
in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

inet_diag TCP dumps currently execute attacker-controlled
INET_DIAG_REQ_BYTECODE programs while still holding the listener,
bind, or ehash bucket locks in tcp_diag_dump(). If the bucket is
heavily populated and the bytecode is a large reject-all filter, the
dump path can spend an unbounded amount of time under the same bucket
lock while walking attacker-arranged sockets.

The reproduced listener case places 131072 SO_REUSEPORT listeners onto
one colliding listener bucket and then issues a NETLINK_SOCK_DIAG dump
request with 16380 INET_DIAG_BC_NOP instructions followed by a failing
INET_DIAG_BC_D_EQ test. Because every socket runs the full bytecode and
none reaches the reply fill path, skb backpressure does not terminate the
walk early. On the unfixed kernel this triggers a watchdog soft lockup
and then a panic in inet_diag_bc_sk().

The earlier batching fix direction was still too narrow: it only counted
sockets that survived the cheap prefilters and reached the expensive dump
path. An attacker can therefore populate one bucket with many sockets or
listeners that fail the netns/family/port or MPTCP-specific prefilters,
causing the same bucket lock to be scanned far past the 16-entry batch
threshold before control is returned.

The same root cause also exists in MPTCP listener dumping. The
MPTCP-specific mptcp_diag_dump_listeners() path reuses sk_diag_dump(),
which runs inet_diag_bc_sk() before filling the netlink reply, while the
listener bucket lock is still held.

We additionally profiled the MPTCP-specific path locally with a
separate debugging artifact. Because struct inet_diag_req_v2 stores
sdiag_protocol in an __u8 field, that request must keep
sdiag_protocol = IPPROTO_TCP and pass INET_DIAG_REQ_PROTOCOL =
u32(IPPROTO_MPTCP), otherwise 262 truncates back to TCP and never reaches
the MPTCP handler. With that corrected request, a 1-group run with 32768
listeners and 16380 NOP bytecode steps took 2082.608 ms on the fixed
kernel versus 8.601 ms on the unfixed kernel, and kprobe profiling showed
inet_diag_bc_sk() executing 32768 times on the fixed kernel but only 256
times on the unfixed one. That 256-count is not a missed path: the
unfixed mptcp_diag_dump_listeners() reuses one counter both as the
in-bucket index and as the resume cursor, so each dump restart advances
in a triangular skip pattern and stops after approximately sqrt(2N)
bytecode executions. The inline reproducer and decoded crash log below
cover the TCP panic path; the MPTCP results above are included only as
supporting local validation for the second patch.

This series fixes both sites by keeping bucket-locked sections limited to
raw socket collection and lifetime pinning, and moving all filtering,
inet_diag_bc_sk(), and socket filling work out of the locked regions so
the batch limit applies to raw bucket traversal itself. For TCP listener,
bind, and ehash buckets, and for the MPTCP listener bucket, restarts now
keep a referenced dump cursor so the next batch resumes after the
previous socket instead of rescanning the bucket head under the same
lock.

For the TCP patch, the underlying root cause predates modern git history.
The Fixes tag therefore uses commit 1da177e4c3f4
("Linux-2.6.12-rc2") as the earliest git-import boundary that still
anchors the pre-git bug in the current repository, rather than
incorrectly attributing it to a later helper or refactor commit. The
MPTCP patch uses the real introduction boundary, commit 4fa39b701ce9
("mptcp: listen diag dump support").

Reproducer:

     gcc -O2 -static -o poc poc.c
     unshare -Urn ./poc

For the reproduced panic log below, we enabled
softlockup panic sysctls and ran the local helper that executes:

     ./poc --listen --groups 4 --stride 2048 --count 32768 --nops 16380

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN poc.c------
#define _GNU_SOURCE

#
[...]
static size_t build_request(void *buf, bool listen_mode, bool with_attack,
			    unsigned int nops)
{
	size_t msg_len = NLMSG_SPACE(sizeof(struct inet_diag_req_v2));
	struct nlmsghdr *nlh = buf;
	struct inet_diag_req_v2 *req;

	memset(buf, 0, msg_len);
	nlh->nlmsg_len = msg_len;
	nlh->nlmsg_type = SOCK_DIAG_BY_FAMILY;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
	nlh->nlmsg_seq = 1;

	req = NLMSG_DATA(nlh);
	req->sdiag_family = AF_INET;
	req->sdiag_protocol = IPPROTO_TCP;
	req->idiag_states = listen_mode ? TCPF_LISTEN : (1U << 12);
1U << 12

12 is TCP_NEW_SYN_RECV. Is TCP_BOUND_INACTIVE what you expected ?

	req->id.idiag_cookie[0] = INET_DIAG_NOCOOKIE;
	req->id.idiag_cookie[1] = INET_DIAG_NOCOOKIE;
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help