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;