[PATCH net-next 1/2] net/sched: act_gate: reject oversized dumps instead of wrapping them
flat view
WARM1d
REVIEWED: 1 (0M)
From: Jamal Hadi Salim <jhs@mojatatu.com>
Date: 2026-10-07 10:29:38
Also in:
bpf
Subsystem:
bpf [general] (safe dynamic programs and tools), bpf [networking] (tcx & tc bpf, sock_addr), networking [general], tc subsystem, the rest · Maintainers:
Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Jamal Hadi Salim, Jiri Pirko, Linus Torvalds
1 review trailer.
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/ (local)
Link: https://lore.kernel.org/netdev/179111893181.434549.13449660670267048994@kernel.org/ (local)
Reviewed-by: Victor Nogueira <redacted>
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