Re: [PATCH net 0/2] tcp: diag: fix unbounded bucket lock hold in diag dump paths
Jiayuan Chen <[email protected]> Wed, 29 Jul 2026 21:55:32 +0800
| Newsgroups | dev.linux.lists.mptcp,org.kernel.vger.netdev |
|---|---|
| Message-ID | <[email protected]> |
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;