net/sched: release old template when replacing a chain
Subsystems (3)
PATCH 1/1 net/sched: release old template when replacing a chain
Hello,
An action-held traffic-control chain can remain alive after RTM_DELCHAIN because a goto-chain action still references it. RTM_NEWCHAIN can reuse the chain. When a new template is supplied, tc_chain_tmplt_add() creates a template and overwrites the existing chain->tmplt_ops and chain->tmplt_priv without destroying the old one.
The old struct fl_flow_tmplt allocation (1160 bytes) becomes unreachable, along with its classifier module reference. Each repeated chain recreation leaks one object from kmalloc-2k. The allocation uses GFP_KERNEL without __GFP_ACCOUNT, and the tested objects remain after the network namespace that created them is destroyed.
A local user with permission to create unprivileged user and network namespaces can obtain CAP_NET_ADMIN inside the child network namespace and trigger the leak without packet traffic or a race. The demonstrated impact is unbounded host-global kernel memory consumption; no memory corruption or privilege escalation is demonstrated.
Two fresh QEMU boots of the unpatched Linux 7.3 tree showed kmalloc-2k active objects increasing from 60 to 192 after 128 cycles. The no-action and held-chain/no-template controls remained at 60. A UID 1000 user+network namespace run increased the count from 48 to 204; the matching control increased it from 48 to 84.
With this patch, two fresh 128-cycle positive boots remained at 60, and both controls remained at 60. The user-namespace positive and control runs both showed the same 48-to-84 setup delta, with no per-cycle increase. The positive PoC reports a failed leak threshold on the patched kernel because it is written to expect the vulnerable behavior; the zero delta is the expected fixed result.
The issue was found during an LLM-assisted source review and confirmed with a userspace PoC in a disposable QEMU VM.
HEAD commit: c03c1a14507dfaf8b856c2608f9ac216021fc222 Git tree: 30fd2ef82912c26cf3a58ad0ba6afc375fa851ac Source version: Linux 7.3.0 Compiler: gcc 11.4.0, GNU ld 2.38
The console output, kernel config, reproducer, namespace launcher, and patched-kernel validation logs are available here:
Console output:
https://gist.githubusercontent.com/shaos26/3e31c231ca56bc53ef80b5c8aada2821/raw/7ff5d84b07e0b260952d2f8575ffb1936edcb099/console-output.txt
Kernel config:
https://gist.githubusercontent.com/shaos26/3e31c231ca56bc53ef80b5c8aada2821/raw/aeed6e5360a23b7e36cc06272dc0069f8639c61f/kernel-config.txt
Reproducer:
https://gist.githubusercontent.com/shaos26/3e31c231ca56bc53ef80b5c8aada2821/raw/f73bf112b5d6bd7e6633963e3dee0c0083ffeef0/reproducer.sh
User namespace launcher:
https://gist.githubusercontent.com/shaos26/3e31c231ca56bc53ef80b5c8aada2821/raw/b30237aa0b52e39ebb9da06d790c3f8430b62903/userns_launcher.c
Patched validation:
https://gist.githubusercontent.com/shaos26/3e31c231ca56bc53ef80b5c8aada2821/raw/d769d8afbfa6293e3f7bff967a3408521972dcb5/patch-validation.txt
Run the reproducer as root, or with CAP_NET_ADMIN in a disposable network namespace, on an unpatched kernel with BusyBox and iproute2:
./reproducer.sh positive 128
./reproducer.sh control-no-action 128
./reproducer.sh control-held-no-template 128
The user-namespace test used userns_launcher.c inside the test initramfs. It drops to UID 1000 before creating the user and network namespaces, then runs the same positive trigger.
The patch creates a replacement template first, leaving the old one intact if creation fails. After successful creation it destroys the old template and drops its module reference before installing the new pointer pair.
--- net/sched/cls_api.c | 2 ++ 1 file changed, 2 insertions(+)
net/sched/cls_api.c +2 -0
@@ -3128,6 +3128,8 @@ static int tc_chain_tmplt_add(struct tcf_chain *chain, struct net *net,module_put (ops->owner);return PTR_ERR (tmplt_priv); } +/* An action-held chain can retain a template after RTM_DELCHAIN. */ +tc_chain_tmplt_del (chain->tmplt_ops, chain->tmplt_priv); chain->tmplt_ops = ops; chain->tmplt_priv = tmplt_priv;return 0 ;
[email protected]
AI reviewThank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider.
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider.
Critical: 0 · High: 1 · Medium: 2 · Low: 1
If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough).
- [High] The patch adds a use-after-free race on chain->tmplt_priv.
- [Medium] The patch treats a symptom.
- [Medium] tc_chain_tmplt_add() creates and offloads the new template (fl_tmplt_create() -> fl_hw_create_tmplt() sends…
- [Low] The commit message reads like a bug-report email rather than a kernel changelog.
--
net/sched: release old template when replacing a chain
With this patch, tc_chain_tmplt_add() destroys any template already on the chain once the new template has been created. It targets a chain that a goto-chain action keeps alive after RTM_DELCHAIN and that RTM_NEWCHAIN then recreates. In that case the old template and its module reference used to leak.
Quoted 1 line
Hello,
[ ... ]
Quoted 7 lines
HEAD commit: c03c1a14507dfaf8b856c2608f9ac216021fc222 Git tree: 30fd2ef82912c26cf3a58ad0ba6afc375fa851ac Source version: Linux 7.3.0 Compiler: gcc 11.4.0, GNU ld 2.38The console output, kernel config, reproducer, namespace launcher, and patched-kernel validation logs are available here:
[ ... ]
[Severity: Low]
This isn't a bug, but the changelog reads like a bug report email. It has the "Hello," greeting, reproducer instructions, the gist URLs, QEMU test logistics, the reporter's tree SHAs and compiler versions. It also has the note that the positive PoC reports a failed threshold on the patched kernel.
Could this material go below the --- line or into a cover letter, so that the changelog only covers the problem, its impact and the fix?
net/sched/cls_api.c static int tc_chain_tmplt_add(struct tcf_chain *chain, struct net *net, · line 3146
Quoted 9 lines
net/sched/cls_api.c +1
-0@@ -3143,6 +3143,8 @@ static int tc_chain_tmplt_add(struct tcf_chain *chain, struct net *net,module_put (ops->owner);return PTR_ERR (tmplt_priv); } +/* An action-held chain can retain a template after RTM_DELCHAIN. */
[Severity: Medium]
Should the template survive RTM_DELCHAIN at all? Right now __tcf_chain_put() only releases it when the chain is finally freed:
net/sched/cls_api.c:__tcf_chain_put() {
...
if (refcnt == 0) {
tc_chain_tmplt_del(tmplt_ops, tmplt_priv);
tcf_chain_destroy(chain, free_block);
}
}While action_refcnt is non-zero, the delete notification is still sent through tc_chain_notify_delete(). The chain is then hidden from dumps, RTM_GETCHAIN and RTM_DELCHAIN, but its template stays active.
This has a few side effects that the patch doesn't address.
After chain N is deleted, tc_new_tfilter() still gets the same chain through tcf_chain_get(block, N, true). It rejects other filter kinds with "Chain template is set to a different filter kind":
if (chain->tmplt_ops && chain->tmplt_ops != tp->ops) {An RTM_NEWCHAIN without TCA_KIND on that chain takes this early return in tc_chain_tmplt_add(), so the recreated chain silently inherits the old template:
/* If kind is not set, user did not specify template. */ if (!tca[TCA_KIND]) return 0;
The old template's hardware offload also stays installed after the delete.
The new comment describes this retention as expected behaviour. Would it be better to release the template when the chain becomes action-only, with suitable synchronization against readers? That would fix the leak and these cases, and this hunk would not be needed.
net/sched/cls_api.c static int tc_chain_tmplt_add(struct tcf_chain *chain, struct net *net, · line 3132
Quoted 1 line
net/sched/cls_api.c +1
-0+tc_chain_tmplt_del (chain->tmplt_ops, chain->tmplt_priv);
[Severity: High]
Can this free the old struct fl_flow_tmplt while an unlocked filter add is still reading it?
tc_ctl_chain() drops block->lock before it calls tc_chain_tmplt_add(), so only rtnl is held at this point. RTM_NEWTFILTER is RTNL_FLAG_DOIT_UNLOCKED. For flower on ingress/clsact or on a shared block, tc_new_tfilter() skips rtnl_lock() altogether. fl_change() then reads tp->chain->tmplt_priv without any lock.
CPU0 (RTM_NEWCHAIN, holds only rtnl)tc_ctl_chain()
chain->explicitly_created = true;
mutex_unlock(&block->lock);CPU1 (flower RTM_NEWTFILTER on chain N, no rtnl)tc_new_tfilter()
tcf_chain_get(block, N, true);
passes the chain->tmplt_ops check
fl_change()
loads tp->chain->tmplt_priv (the old template)CPU0tc_chain_tmplt_add()
ops->tmplt_create()
tc_chain_tmplt_del()
fl_tmplt_destroy()
fl_hw_destroy_tmplt(tmplt->chain, tmplt);
kfree(tmplt);CPU1fl_mask_fits_tmplt(tp->chain->tmplt_priv, mask)
reads tmplt->mask from the freed object
The free also happens before the new pointers are stored. A reader that loads chain->tmplt_priv between the kfree() and this store gets a pointer that is already dangling:
chain->tmplt_priv = tmplt_priv;
Before this patch, the only template free was in __tcf_chain_put() at refcnt == 0, when nothing else could hold the chain. The commit message notes that a user namespace is enough for CAP_NET_ADMIN, and running DELCHAIN/NEWCHAIN cycles next to concurrent flower filter adds would reach this window.
[Severity: Medium]
With this order, fl_hw_create_tmplt() sends FLOW_CLS_TMPLT_CREATE for the new template before fl_hw_destroy_tmplt() sends FLOW_CLS_TMPLT_DESTROY for the old one. Both use the same chain_index. Can that leave offload drivers in the wrong state?
The in-tree drivers seem to assume one template per chain index, and they match DESTROY by chain_index only. In sparx5, create records the current port keyset as orig:
drivers/net/ethernet/microchip/sparx5/sparx5_tc_flower.c:sparx5_tc_flower_template_create() {
...
sparx5_vcap_set_port_keyset(ndev, admin, fco->common.chain_index,
state.l3_proto,
ftp->keyset,
&kslist);
if (kslist.cnt > 0)
ftp->orig = kslist.keysets[0];
...
}Here orig is the old template's keyset. Destroy then restores orig from the first entry with a matching cid, which is the old template's entry:
drivers/net/ethernet/microchip/sparx5/sparx5_tc_flower.c:sparx5_tc_flower_template_destroy() {
...
list_for_each_entry_safe(ftp, tmp, &port->tc_templates, list) {
if (ftp->cid != fco->common.chain_index)
continue;
sparx5_vcap_set_port_keyset(ndev, admin,
fco->common.chain_index,
ftp->l3_proto, ftp->orig,
NULL);
...
}So the port runs the keyset from before any template while the new template is active. Once the new template is destroyed, the port goes back to the old template's keyset.
In mlxsw, mlxsw_sp_flower_tmplt_create() reuses the existing ruleset for the chain index through mlxsw_sp_acl_ruleset_get(), so the new template's elusage is ignored. In prestera, prestera_acl_ruleset_keymask_set() overwrites ruleset->keymask with kmemdup() without freeing the previous one, so each cycle leaks one keymask.
The mlxsw and prestera behaviour already existed before this patch, but create-first keeps it, while destroy-then-create would let the ruleset be released and rebuilt.
The commit message says create-first keeps the old template intact if creation fails. Would destroying the old template first give drivers a consistent sequence?
Quoted 3 lines
net/sched/cls_api.c +0
-0chain->tmplt_ops = ops; chain->tmplt_priv = tmplt_priv;return 0 ;
Signature · 1 line
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109121448.2589374-1-shawnq1%40163.com