From: Jamal Hadi Salim <jhs@mojatatu.com>
To: netdev@vger.kernel.org
Cc: Jamal Hadi Salim <jhs@mojatatu.com>,
Victor Nogueira <victor@mojatatu.com>,
Jiri Pirko <jiri@resnulli.us>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@kernel.org>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Simon Horman <horms@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Martin KaFai Lau <martin.lau@linux.dev>,
John Fastabend <john.fastabend@gmail.com>,
Stanislav Fomichev <sdf@fomichev.me>,
bpf@vger.kernel.org, sashiko-bot@kernel.org
Subject: [PATCH net-next 1/2] net/sched: act_gate: reject oversized dumps instead of wrapping them
Date: Wed, 7 Oct 2026 06:29:30 -0400 [thread overview]
Message-ID: <QDISC-H19Z.v2.20261007062551-2@mojatatu.com> (raw)
In-Reply-To: <QDISC-H19Z.v2.20261007062551@mojatatu.com>
This is a followup of commit cfa165cbfbed ("net/sched: act_gate: budget
the per-entry list in get_fill_size") as reported by Sashiko.
The issue:
An action dump is carried inside nested netlink attributes whose lengths
are u16. A gate action with enough schedule entries produces a dump
larger than U16_MAX, and the plain nla_nest_end() closes by writing a
wrapped length: the reply is structurally corrupt. On a CONFIG_DEBUG_NET
kernel nla_nest_end() also warns for each wrapped close, which an
unprivileged RTM_GETACTION or RTM_GETTFILTER can reach once the action
or filter exists.
The oversized dump also reaches the filter notification path through
tcf_fill_node(), which reports the failure as -EMSGSIZE.
tfilter_notify_prep() treats that as socket-buffer exhaustion and
retries with an ever larger alloc_skb(); a dump that does not fit a u16
nest cannot be built at any skb size, so the loop only spins until the
allocation itself fails.
The Fix:
Close every wrap-capable nest an action dump travels in with
nla_nest_end_safe(), which reports -EMSGSIZE before writing a wrapped
length: TCA_GATE_ENTRY_LIST, TCA_ACT_OPTIONS, the per-action nest and
TCA_ACT_TAB; the shared action and police containers in
tcf_exts_dump()/tcf_exts_terse_dump(); and the caller-owned TCA_OPTIONS
of every action-capable classifier (flower, matchall, basic, bpf,
cgroup, flow, fw, route4, u32). The output is byte identical for every
message that serializes. Bound the tfilter_notify_prep() retry once the
skb is already larger than any valid message, while a regular dump that
only needs a bigger skb still retries.
Context note:
A notification that cannot be serialised must not veto the state change
the caller asked for. tfilter_del_notify() builds the delete
notification before calling ->delete() and propagated the prep
failure, so a filter whose dump cannot be represented in a u16 nest
could be created but never removed by "tc filter del ... handle H" - and
only when a notification was actually needed (rtnl_notify_needed(): an
RTNLGRP_TC listener or NLM_F_ECHO), which made it intermittent. Drop
the unbuildable notification instead: on -EMSGSIZE the delete proceeds
and the extack notes the dropped event. The same serialisation failure
on the action-add side - tcf_action_add() returns -EINVAL although
tcf_action_init() has already inserted the action into the idr, so the
action is created and reported as a failure - is not changed here:
returning success after a notify build failure there needs an
idr/refcount audit (tca_put_bound_many(), ACT_P_CREATED) that overlaps
the tcf_action_add_failed_notify_leaves_action follow-up. Preserve the
-EMSGSIZE tcf_action_dump() produces (goto errout, like
rtnl_fill_prop_list()) instead of overwriting it with -EINVAL.
why net-next?
This is hardening rather than a regression fix: the enclosing action and
filter-path nests have wrapped since the actions and classifiers allowed
large entry lists - TCA_ACT_TAB is core act_api and the filter-side
container predates the act_gate sizing change. A plain nla_nest_end()
has always written the wrapped length at U16_MAX; this stops the
corruption at the abstraction boundary instead of imposing a new policy
limit on accepted gate schedules. A separate patch caps the entry list
itself.
Conditions to recreate the bug:
CONFIG_NET_CLS_ACT=y; CONFIG_NET_ACT_GATE=y;
CONFIG_NET_CLS_MATCHALL=y and/or CONFIG_NET_CLS_BASIC=y. Create a
matchall or basic filter on an ingress/clsact qdisc whose action list
holds two gate actions of 1024 minimal TCA_GATE_ONE_ENTRY elements
each (TCA_ACT_MAX_PRIO admits up to 32 actions); the aggregate
TCA_ACT_TAB nest is ~74.3 KiB > 65532 and wraps, then RTM_GETTFILTER
it (or trigger the notify with NLM_F_ECHO).
For a single action the outermost TCA_ACT_TAB wraps from 1815 entries
(1821 for the innermost TCA_GATE_ENTRY_LIST); on the filter path the
outermost wrap-capable nest is the classifier's TCA_OPTIONS. Installing
needs CAP_NET_ADMIN in a user namespace; the RTM_GETTFILTER trigger needs
no capability once it exists. The two-action shape survives the
series' per-action 1024-entry cap (2/2): the enclosing nests overflow
from aggregate action size, which no per-action cap can bound.
Reported-by: Sashiko (nipa) <sashiko-bot@kernel.org>
Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260824153903.4143642-1-victor@mojatatu.com
Link: https://lore.kernel.org/netdev/QDISC-H19Z.v1.20261001053234@mojatatu.com/
Link: https://lore.kernel.org/netdev/179111893181.434549.13449660670267048994@kernel.org/
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
net/sched/act_api.c | 10 +++++++---
net/sched/act_gate.c | 3 ++-
net/sched/cls_api.c | 40 ++++++++++++++++++++++++++++++----------
net/sched/cls_basic.c | 3 ++-
net/sched/cls_bpf.c | 3 ++-
net/sched/cls_cgroup.c | 3 ++-
net/sched/cls_flow.c | 3 ++-
net/sched/cls_flower.c | 6 ++++--
net/sched/cls_fw.c | 3 ++-
net/sched/cls_matchall.c | 3 ++-
net/sched/cls_route.c | 3 ++-
net/sched/cls_u32.c | 3 ++-
12 files changed, 59 insertions(+), 24 deletions(-)
diff --git a/net/sched/act_api.c b/net/sched/act_api.c
index 6e48b4bc2d75..747d91ae6446 100644
--- a/net/sched/act_api.c
+++ b/net/sched/act_api.c
@@ -558,7 +558,8 @@ tcf_action_dump_1(struct sk_buff *skb, struct tc_action *a, int bind, int ref)
goto nla_put_failure;
err = tcf_action_dump_old(skb, a, bind, ref);
if (err > 0) {
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
return err;
}
@@ -1279,7 +1280,9 @@ int tcf_action_dump(struct sk_buff *skb, struct tc_action *actions[],
tcf_action_dump_1(skb, a, bind, ref);
if (err < 0)
goto errout;
- nla_nest_end(skb, nest);
+ err = nla_nest_end_safe(skb, nest);
+ if (err < 0)
+ goto errout;
}
return 0;
@@ -1693,7 +1696,8 @@ static int tca_get_fill(struct sk_buff *skb, struct tc_action *actions[],
if (tcf_action_dump(skb, actions, bind, ref, false) < 0)
goto out_nlmsg_trim;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto out_nlmsg_trim;
nlh->nlmsg_len = skb_tail_pointer(skb) - b;
diff --git a/net/sched/act_gate.c b/net/sched/act_gate.c
index 6d6d45e03c07..14801c604bd9 100644
--- a/net/sched/act_gate.c
+++ b/net/sched/act_gate.c
@@ -654,7 +654,8 @@ static int tcf_gate_dump(struct sk_buff *skb, struct tc_action *a,
goto nla_put_failure;
}
- nla_nest_end(skb, entry_list);
+ if (nla_nest_end_safe(skb, entry_list) < 0)
+ goto nla_put_failure;
tcf_tm_dump(&t, &gact->tcf_tm);
if (nla_put_64bit(skb, TCA_GATE_TM, sizeof(t), &t, TCA_GATE_PAD))
diff --git a/net/sched/cls_api.c b/net/sched/cls_api.c
index a9f54988561f..3f0ce567fa2d 100644
--- a/net/sched/cls_api.c
+++ b/net/sched/cls_api.c
@@ -2149,11 +2149,20 @@ static struct sk_buff *tfilter_notify_prep(struct net *net,
rtnl_held, extack);
if (ret <= 0) {
kfree_skb(skb);
- if (ret == -EMSGSIZE) {
- size += NLMSG_GOODSIZE;
- goto retry;
- }
- return ERR_PTR(-EINVAL);
+ if (ret != -EMSGSIZE)
+ return ERR_PTR(-EINVAL);
+ /* A filter dump is carried inside a nest whose u16 nla_len
+ * caps it, so a dump that still does not serialize once the
+ * skb is larger than any valid message can never be built,
+ * however big the skb gets. Filling reports that structural
+ * overflow and genuine capacity exhaustion the same way, so
+ * stop at the bound instead of looping until alloc_skb()
+ * fails on an order too large for the page allocator.
+ */
+ if (size > U16_MAX + NLMSG_GOODSIZE)
+ return ERR_PTR(-EMSGSIZE);
+ size += NLMSG_GOODSIZE;
+ goto retry;
}
return skb;
}
@@ -2200,8 +2209,16 @@ static int tfilter_del_notify(struct net *net, struct sk_buff *oskb,
skb = tfilter_notify_prep(net, oskb, n, tp, block, q, parent, fh,
RTM_DELTFILTER, portid, rtnl_held, extack);
if (IS_ERR(skb)) {
- NL_SET_ERR_MSG(extack, "Failed to build del event notification");
- return PTR_ERR(skb);
+ if (PTR_ERR(skb) != -EMSGSIZE) {
+ NL_SET_ERR_MSG(extack, "Failed to build del event notification");
+ return PTR_ERR(skb);
+ }
+ /* The filter's dump cannot be represented in a u16 nest, so no
+ * notification can ever be built for it. Drop the notification
+ * rather than refusing to delete the filter.
+ */
+ NL_SET_ERR_MSG(extack, "Filter deleted; del event notification could not be built");
+ return tp->ops->delete(tp, fh, last, rtnl_held, extack);
}
err = tp->ops->delete(tp, fh, last, rtnl_held, extack);
@@ -3529,7 +3546,8 @@ int tcf_exts_dump(struct sk_buff *skb, struct tcf_exts *exts)
if (tcf_action_dump(skb, exts->actions, 0, 0, false)
< 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
} else if (exts->police) {
struct tc_action *act = tcf_exts_first_act(exts);
nest = nla_nest_start_noflag(skb, exts->police);
@@ -3537,7 +3555,8 @@ int tcf_exts_dump(struct sk_buff *skb, struct tcf_exts *exts)
goto nla_put_failure;
if (tcf_action_dump_old(skb, act, 0, 0) < 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
}
}
return 0;
@@ -3565,7 +3584,8 @@ int tcf_exts_terse_dump(struct sk_buff *skb, struct tcf_exts *exts)
if (tcf_action_dump(skb, exts->actions, 0, 0, true) < 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
return 0;
nla_put_failure:
diff --git a/net/sched/cls_basic.c b/net/sched/cls_basic.c
index e2a94ba9fba7..ba859110faae 100644
--- a/net/sched/cls_basic.c
+++ b/net/sched/cls_basic.c
@@ -305,7 +305,8 @@ static int basic_dump(struct net *net, struct tcf_proto *tp, void *fh,
tcf_em_tree_dump(skb, &f->ematches, TCA_BASIC_EMATCHES) < 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &f->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_bpf.c b/net/sched/cls_bpf.c
index 188cf0f949dd..232796d3a51f 100644
--- a/net/sched/cls_bpf.c
+++ b/net/sched/cls_bpf.c
@@ -631,7 +631,8 @@ static int cls_bpf_dump(struct net *net, struct tcf_proto *tp, void *fh,
nla_put_u32(skb, TCA_BPF_FLAGS_GEN, prog->gen_flags))
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &prog->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_cgroup.c b/net/sched/cls_cgroup.c
index 210fd9fd26d8..28701d3916b8 100644
--- a/net/sched/cls_cgroup.c
+++ b/net/sched/cls_cgroup.c
@@ -185,7 +185,8 @@ static int cls_cgroup_dump(struct net *net, struct tcf_proto *tp, void *fh,
tcf_em_tree_dump(skb, &head->ematches, TCA_CGROUP_EMATCHES) < 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &head->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_flow.c b/net/sched/cls_flow.c
index a9ac3acf6eda..2ef449706638 100644
--- a/net/sched/cls_flow.c
+++ b/net/sched/cls_flow.c
@@ -681,7 +681,8 @@ static int flow_dump(struct net *net, struct tcf_proto *tp, void *fh,
tcf_em_tree_dump(skb, &f->ematches, TCA_FLOW_EMATCHES) < 0)
goto nla_put_failure;
#endif
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &f->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_flower.c b/net/sched/cls_flower.c
index 0c4beff18d68..299b1493d74b 100644
--- a/net/sched/cls_flower.c
+++ b/net/sched/cls_flower.c
@@ -3759,7 +3759,8 @@ static int fl_dump(struct net *net, struct tcf_proto *tp, void *fh,
if (tcf_exts_dump(skb, &f->exts))
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &f->exts) < 0)
goto nla_put_failure;
@@ -3804,7 +3805,8 @@ static int fl_terse_dump(struct net *net, struct tcf_proto *tp, void *fh,
if (tcf_exts_terse_dump(skb, &f->exts))
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
return skb->len;
diff --git a/net/sched/cls_fw.c b/net/sched/cls_fw.c
index a462b262719c..0f731595f839 100644
--- a/net/sched/cls_fw.c
+++ b/net/sched/cls_fw.c
@@ -414,7 +414,8 @@ static int fw_dump(struct net *net, struct tcf_proto *tp, void *fh,
if (tcf_exts_dump(skb, &f->exts) < 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &f->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_matchall.c b/net/sched/cls_matchall.c
index c14899b935bf..6ece63b82775 100644
--- a/net/sched/cls_matchall.c
+++ b/net/sched/cls_matchall.c
@@ -366,7 +366,8 @@ static int mall_dump(struct net *net, struct tcf_proto *tp, void *fh,
if (tcf_exts_dump(skb, &head->exts))
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &head->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_route.c b/net/sched/cls_route.c
index 0f211f030fd9..d5b009a7c89b 100644
--- a/net/sched/cls_route.c
+++ b/net/sched/cls_route.c
@@ -657,7 +657,8 @@ static int route4_dump(struct net *net, struct tcf_proto *tp, void *fh,
if (tcf_exts_dump(skb, &f->exts) < 0)
goto nla_put_failure;
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (tcf_exts_dump_stats(skb, &f->exts) < 0)
goto nla_put_failure;
diff --git a/net/sched/cls_u32.c b/net/sched/cls_u32.c
index 76ce2d124079..91f4e6458785 100644
--- a/net/sched/cls_u32.c
+++ b/net/sched/cls_u32.c
@@ -1482,7 +1482,8 @@ static int u32_dump(struct net *net, struct tcf_proto *tp, void *fh,
#endif
}
- nla_nest_end(skb, nest);
+ if (nla_nest_end_safe(skb, nest) < 0)
+ goto nla_put_failure;
if (TC_U32_KEY(n->handle))
if (tcf_exts_dump_stats(skb, &n->exts) < 0)
--
2.43.0
next prev parent reply other threads:[~2026-10-07 10:29 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-07 10:29 [PATCH net-next 0/2] net/sched: act_gate: reject oversized dumps and cap entry count Jamal Hadi Salim
2026-10-07 10:29 ` Jamal Hadi Salim [this message]
2026-10-07 10:29 ` [PATCH net-next 2/2] net/sched: act_gate: cap the number of scheduling entries Jamal Hadi Salim
2026-10-07 14:23 ` Paul Moses
2026-10-08 7:51 ` Jamal Hadi Salim
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=QDISC-H19Z.v2.20261007062551-2@mojatatu.com \
--to=jhs@mojatatu.com \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=horms@kernel.org \
--cc=jiri@resnulli.us \
--cc=john.fastabend@gmail.com \
--cc=kuba@kernel.org \
--cc=martin.lau@linux.dev \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=sashiko-bot@kernel.org \
--cc=sdf@fomichev.me \
--cc=victor@mojatatu.com \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox