* [PATCH net-next 1/2] net/sched: act_gate: reject oversized dumps instead of wrapping them
[not found] <QDISC-H19Z.v2.20261007062551@mojatatu.com>
@ 2026-10-07 10:29 ` Jamal Hadi Salim
2026-10-08 10:29 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Jamal Hadi Salim @ 2026-10-07 10:29 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Victor Nogueira, Jiri Pirko, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Daniel Borkmann, Martin KaFai Lau, John Fastabend,
Stanislav Fomichev, bpf, sashiko-bot
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
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH net-next 1/2] net/sched: act_gate: reject oversized dumps instead of wrapping them
2026-10-07 10:29 ` [PATCH net-next 1/2] net/sched: act_gate: reject oversized dumps instead of wrapping them Jamal Hadi Salim
@ 2026-10-08 10:29 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-10-08 10:29 UTC (permalink / raw)
To: Jamal Hadi Salim; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] net/sched: action leaks in IDR when tcf_add_notify fails
--
commit c1aaa71f5e62d0438d19f3c77432158d3dd8ca67
Author: Jamal Hadi Salim <jhs@mojatatu.com>
net/sched: act_gate: reject oversized dumps instead of wrapping them
This patch limits oversized action dumps by closing wrap-capable nests with
nla_nest_end_safe(). It prevents structural corruption of Netlink messages
by failing with -EMSGSIZE instead of writing a wrapped length above U16_MAX.
Link: https://lore.kernel.org/netdev/QDISC-H19Z.v1.20261001053234@mojatatu.com/
Link: https://lore.kernel.org/netdev/179111893181.434549.13449660670267048994@kernel.org/
> diff --git a/net/sched/act_api.c b/net/sched/act_api.c
> index 6e48b4bc2d75d..747d91ae6446a 100644
> --- a/net/sched/act_api.c
> +++ b/net/sched/act_api.c
[ ... ]
> @@ -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;
[Severity: Medium]
If tcf_add_notify() fails due to the new -EMSGSIZE limit introduced here
for oversized dumps, tcf_action_add() propagates the error to its caller.
However, in tcf_action_add(), tca_put_bound_many() explicitly skips
cleaning up actions where init_res[i] == ACT_P_CREATED:
ret = tcf_add_notify(net, n, actions, portid, attr_size, extack);
/* only put bound actions */
tca_put_bound_many(actions, init_res);
return ret;
This appears to leave the newly created actions fully instantiated in the IDR
with an active reference, while userspace receives an error and assumes the
creation failed.
Does this regression cause ACT_P_CREATED actions to leak in the IDR?
The commit message acknowledges this serialization failure on the action-add
side, but leaves the necessary follow-up fix out of this patch series. Could
this state inconsistency be triggered by userspace to leak actions before
the follow-up patch is merged?
--
Sashiko AI review · https://sashiko.dev/#/patchset/QDISC-H19Z.v2.20261007062551@mojatatu.com?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-08 10:29 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <QDISC-H19Z.v2.20261007062551@mojatatu.com>
2026-10-07 10:29 ` [PATCH net-next 1/2] net/sched: act_gate: reject oversized dumps instead of wrapping them Jamal Hadi Salim
2026-10-08 10:29 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox