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) 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 Signed-off-by: Jamal Hadi Salim --- 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