[PATCH net] inet_diag: make bytecode audit O(n) to prevent CPU DoS
From: Yizhou Zhao <hidden>
Date: 2026-07-27 15:39:59
Also in:
lkml, stable
Subsystem:
networking [general], the rest · Maintainers:
"David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
inet_diag: make bytecode audit O(n) to prevent CPU DoS
inet_diag_bc_audit() validates the user-supplied INET_DIAG_REQ_BYTECODE
filter of a SOCK_DIAG dump request. It runs synchronously in the sender's
sendmsg() at dump start, before any socket is iterated.
The audit's outer loop walks one opcode per iteration following the
yes-chain, and for every non-NOP opcode whose 'no' branch stays inside the
buffer it calls valid_cc(bytecode, bytecode_len, len - op->no). valid_cc()
re-walks the bytecode from offset 0 following yes-jumps until it reaches
the target offset. This is O(n) per opcode, nested inside the O(n) outer
loop, making the whole audit O(n^2) in the bytecode length.
NETLINK_SOCK_DIAG is reachable by unprivileged users (diag_nlsk is created
with NL_CFG_F_NONROOT_RECV, the SOCK_DIAG_BY_FAMILY dump path has no
capability gate, and netlink_sendmsg skips the NL_CFG_F_NONROOT_SEND check
when dst_portid == 0 && dst_group == 0). The bytecode length is bounded
only by the u16 nla_len cap (~64 KB), so a local unprivileged attacker can
submit a ~64 KB bytecode of JMP ops {yes=4, no=4} that passes every
validation check, is accepted, and forces ~n^2/2 (~1.3e8) inner iterations
per request, pinning a CPU for hundreds of milliseconds; concurrent
requests pin all CPUs. The burn happens at dump start, so it occurs even
when zero sockets match.
Replace the repeated from-start re-walk with a single-pass bitmap
reachability check that preserves the exact semantics of valid_cc().
Allocate one bit per 4-byte slot: when an opcode's 'no' branch targets an
offset strictly inside the buffer (op->no < len), set the bit for that
target; when the yes-chain later arrives at an offset, clear its bit
(proving it is a reachable opcode boundary). After the walk, any bit still
set marks a 'no' target the yes-chain never lands on -- i.e. it points
mid-opcode or at an unreachable op -- which is exactly the condition
valid_cc() rejected, so the audit returns -EINVAL. Each opcode now does
O(1) bitmap work, making the audit O(n).
The bitmap is freed through a single 'out' label, and the now unused
valid_cc() is removed.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Yizhou Zhao <redacted>
Reported-by: Yuxiang Yang <redacted>
Reported-by: Ao Wang <redacted>
Reported-by: Xuewei Feng <redacted>
Reported-by: Qi Li <redacted>
Reported-by: Ke Xu <redacted>
Assisted-by: Claude-Code:GLM-5.2
Signed-off-by: Yizhou Zhao <redacted>
---diff --git a/net/ipv4/inet_diag.c b/net/ipv4/inet_diag.c
index 34b77aa..304f031 100644
--- a/net/ipv4/inet_diag.c
+++ b/net/ipv4/inet_diag.c@@ -11,6 +11,7 @@ #include <linux/fcntl.h> #include <linux/random.h> #include <linux/slab.h> +#include <linux/bitmap.h> #include <linux/cache.h> #include <linux/init.h> #include <linux/time.h>
@@ -632,23 +633,6 @@ int inet_diag_bc_sk(const struct inet_diag_dump_data *cb_data, struct sock *sk) } EXPORT_SYMBOL_GPL(inet_diag_bc_sk); -static int valid_cc(const void *bc, int len, int cc) -{ - while (len >= 0) { - const struct inet_diag_bc_op *op = bc; - - if (cc > len) - return 0; - if (cc == len) - return 1; - if (op->yes < 4 || op->yes & 3) - return 0; - len -= op->yes; - bc += op->yes; - } - return 0; -} - /* data is u32 ifindex */ static bool valid_devcond(const struct inet_diag_bc_op *op, int len, int *min_len)
@@ -729,8 +713,9 @@ static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data, const struct sk_buff *skb) { const struct nlattr *attr = cb_data->inet_diag_nla_bc; - const void *bytecode, *bc; - int bytecode_len, len; + int bytecode_len, len, nbits, err = -EINVAL; + unsigned long *target; + const void *bc; bool net_admin; if (!attr)
@@ -739,23 +724,38 @@ static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data, if (nla_len(attr) < sizeof(struct inet_diag_bc_op)) return -EINVAL; + /* One bit per 4-byte slot tracks 'no'-branch targets that must be + * reached along the yes-chain. + */ + nbits = DIV_ROUND_UP(nla_len(attr), 4); + target = bitmap_zalloc(nbits, GFP_KERNEL); + if (!target) + return -ENOMEM; + net_admin = netlink_net_capable(skb, CAP_NET_ADMIN); - bytecode = bc = nla_data(attr); + bc = nla_data(attr); len = bytecode_len = nla_len(attr); while (len > 0) { int min_len = sizeof(struct inet_diag_bc_op); const struct inet_diag_bc_op *op = bc; + int off = bytecode_len - len; + + /* Reaching 'off' along the yes-chain proves it is a valid + * opcode boundary, satisfying any earlier 'no' branch that + * targeted it. + */ + __clear_bit(off / 4, target); switch (op->code) { case INET_DIAG_BC_S_COND: case INET_DIAG_BC_D_COND: if (!valid_hostcond(bc, len, &min_len)) - return -EINVAL; + goto out; break; case INET_DIAG_BC_DEV_COND: if (!valid_devcond(bc, len, &min_len)) - return -EINVAL; + goto out; break; case INET_DIAG_BC_S_EQ: case INET_DIAG_BC_S_GE:
@@ -764,19 +765,21 @@ static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data, case INET_DIAG_BC_D_GE: case INET_DIAG_BC_D_LE: if (!valid_port_comparison(bc, len, &min_len)) - return -EINVAL; + goto out; break; case INET_DIAG_BC_MARK_COND: - if (!net_admin) - return -EPERM; + if (!net_admin) { + err = -EPERM; + goto out; + } if (!valid_markcond(bc, len, &min_len)) - return -EINVAL; + goto out; cb_data->mark_needed = true; break; #ifdef CONFIG_SOCK_CGROUP_DATA case INET_DIAG_BC_CGROUP_COND: if (!valid_cgroupcond(bc, len, &min_len)) - return -EINVAL; + goto out; cb_data->cgroup_needed = true; break; #endif
@@ -787,23 +790,31 @@ static int inet_diag_bc_audit(struct inet_diag_dump_data *cb_data, case INET_DIAG_BC_NOP: break; default: - return -EINVAL; + goto out; } if (op->code != INET_DIAG_BC_NOP) { if (op->no < min_len || op->no > len + 4 || op->no & 3) - return -EINVAL; - if (op->no < len && - !valid_cc(bytecode, bytecode_len, len - op->no)) - return -EINVAL; + goto out; + /* Record the 'no'-branch target; it must be cleared + * later when the yes-chain lands on it, otherwise the + * target is not a reachable opcode boundary. + */ + if (op->no < len) + __set_bit((off + op->no) / 4, target); } if (op->yes < min_len || op->yes > len + 4 || op->yes & 3) - return -EINVAL; + goto out; bc += op->yes; len -= op->yes; } - return len == 0 ? 0 : -EINVAL; + + if (len == 0 && bitmap_empty(target, nbits)) + err = 0; +out: + bitmap_free(target); + return err; } static int __inet_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, --
2.47.3