Re: [PATCH net 0/2] tcp: diag: fix unbounded bucket lock hold in diag dump paths
zihan xi <[email protected]> Wed, 29 Jul 2026 22:21:02 +0800
| Newsgroups | dev.linux.lists.mptcp,org.kernel.vger.netdev |
|---|---|
| Message-ID | <CAANe3eSDxw=r4OYo5Ch-2+u24wbC7tKyML-arrSd5VfV7zgiUw@mail.gmail.com> |
On Wed, Jul 29, 2026 at 9:55=E2=80=AFPM Jiayuan Chen <[email protected]= v> wrote: > > > 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 t= he > > 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 counte= d > > sockets that survived the cheap prefilters and reached the expensive du= mp > > path. An attacker can therefore populate one bucket with many sockets o= r > > 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 th= e > > 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 =3D IPPROTO_TCP and pass INET_DIAG_REQ_PROTOCOL =3D > > u32(IPPROTO_MPTCP), otherwise 262 truncates back to TCP and never reach= es > > the MPTCP handler. With that corrected request, a 1-group run with 3276= 8 > > 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 show= ed > > inet_diag_bc_sk() executing 32768 times on the fixed kernel but only 25= 6 > > 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 listene= r, > > bind, and ehash buckets, and for the MPTCP listener bucket, restarts no= w > > 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 histor= y. > > 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_atta= ck, > > unsigned int nops) > > { > > size_t msg_len =3D NLMSG_SPACE(sizeof(struct inet_diag_req_v2)); > > struct nlmsghdr *nlh =3D buf; > > struct inet_diag_req_v2 *req; > > > > memset(buf, 0, msg_len); > > nlh->nlmsg_len =3D msg_len; > > nlh->nlmsg_type =3D SOCK_DIAG_BY_FAMILY; > > nlh->nlmsg_flags =3D NLM_F_REQUEST | NLM_F_DUMP; > > nlh->nlmsg_seq =3D 1; > > > > req =3D NLMSG_DATA(nlh); > > req->sdiag_family =3D AF_INET; > > req->sdiag_protocol =3D IPPROTO_TCP; > > req->idiag_states =3D listen_mode ? TCPF_LISTEN : (1U << 12); > > 1U << 12 > > 12 is TCP_NEW_SYN_RECV. Is TCP_BOUND_INACTIVE what you expected ? > Hi, You're right, `1U << 12` is `TCP_NEW_SYN_RECV`, not `TCP_BOUND_INACTIVE`. The inline crash reproducer only exercised the `--listen` path, so this did not affect the reproduced panic there, but the non-listener branch in the PoC is indeed wrong. The intended state there is: req->idiag_states =3D listen_mode ? TCPF_LISTEN : TCPF_BOUND_INACTIVE; I'll include this PoC/cover-letter fix in the next reroll. Thanks for catching this. Best regards, Zihan > > > req->id.idiag_cookie[0] =3D INET_DIAG_NOCOOKIE; > > req->id.idiag_cookie[1] =3D INET_DIAG_NOCOOKIE;