The route4 classifier maintains a 16-slot fastmap cache that stores raw struct route4_filter pointers indexed by (id, iif). The reader (route4_classify) populates this cache via route4_set_fastmap() for every classified packet that hits a filter. The writer (route4_delete, route4_change) clears the cache via route4_reset_fastmap() before RCU-deferred kfree of the filter. This creates a UAF race: 1. Reader walks the RCU-protected bucket chain, finds filter f 2. Writer unlinks f, calls route4_reset_fastmap(), then tcf_queue_work() 3. Reader calls route4_set_fastmap() and writes f into the cache *after* the writer's reset, caching a pointer about to be freed 4. After the RCU grace period, kfree(f) executes 5. Next classified packet on the same (id, iif) tuple hits the stale fastmap entry and reads f->res from freed memory Reproduced with an mdelay(100) accelerator in route4_set_fastmap() and a concurrent add/delete stress test (provided by both zdi and Santosh). Both triggered KASAN slab-use-after-free reports in the route4 fastmap paths. Fix: Move the fastmap flush to the writer side under RTNL, ordered after synchronize_rcu(). This ensures stale fastmap entries written by in-flight readers are visible (readers have finished) and no new readers can find the unlinked filter, so the subsequent route4_reset_fastmap(head) flushes them safely while head is valid under RTNL. In route4_destroy() the synchronous __route4_delete_filter() fallback is retained for the case where tcf_exts_get_net() returns false (netns is being torn down in cleanup_net()). The trailing synchronize_rcu() before kfree_rcu(head) drains any in-flight readers first, so the synchronous free is safe. Removing the fallback and unconditionally deferring via route4_queue_work() would race with tc_action_net_exit(): cleanup_net() does not flush the tc_filter_wq workqueue, so the deferred work could run after the action idrinfo has been freed, causing a use-after-free on idrinfo->lock in __tcf_action_put(). Note: This fix will slow down deletes and filter add/replace, since synchronize_rcu() is called under RTNL in route4_delete() and route4_change(). An alternative fix would have been to introduce refcnt to the head struct but that is a much bigger patch. We justify this as a reasonable fix. Fixes: 1109c00547fc ("net: sched: RCU cls_route") Reported-by: zdi-disclosures@trendmicro.com Reported-by: Santosh Kalluri Suggested-by: Santosh Kalluri Tested-by: Victor Nogueira Tested-by: Santosh Kalluri Signed-off-by: Jamal Hadi Salim --- V1->V2 1. Fix based on feedback from the Sashikos https://sashiko.dev/#/patchset/20260723105210.817079-1-jhs%40mojatatu.com https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260723105210.817079-1-jhs%40mojatatu.com Restore the "if (tcf_exts_get_net(&f->exts)) route4_queue_work(f); else __route4_delete_filter(f);" preserving the tcf_exts_get_net() contract for netns teardown. --- net/sched/cls_route.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/net/sched/cls_route.c b/net/sched/cls_route.c index bd6f945bd388..c7d0164c1e9f 100644 --- a/net/sched/cls_route.c +++ b/net/sched/cls_route.c @@ -307,6 +307,19 @@ static void route4_destroy(struct tcf_proto *tp, bool rtnl_held, kfree_rcu(b, rcu); } } + + /* All filters are unlinked; no new reader can find them on the + * chain. Wait for in-flight readers that may still hold a filter + * pointer and have published it into the fastmap after we unlinked. + * Then flush the stale entries while head is still valid under + * RTNL, matching the route4_delete() pattern. This also protects + * the synchronous __route4_delete_filter() path above: when the + * netns is dying, tcf_exts_get_net() returns false and the filter + * is freed directly, but only after all readers have drained. + */ + synchronize_rcu(); + route4_reset_fastmap(head); + kfree_rcu(head, rcu); } @@ -334,10 +347,16 @@ static int route4_delete(struct tcf_proto *tp, void *arg, bool *last, /* unlink it */ RCU_INIT_POINTER(*fp, rtnl_dereference(f->next)); - /* Remove any fastmap lookups that might ref filter - * notice we unlink'd the filter so we can't get it - * back in the fastmap. + /* This code path assumes we have the RTNL lock. + * We wait for in-flight readers that may still hold + * the filter pointer and publish it into the fastmap + * after we unlinked it. + * Once done, no new reader can find the filter on the + * chain, so the only stale fastmap entries are the + * ones those readers just wrote. Flush them now + * while head is still valid. */ + synchronize_rcu(); route4_reset_fastmap(head); /* Delete it */ @@ -558,6 +577,15 @@ static int route4_change(struct net *net, struct sk_buff *in_skb, } } + /* Assuming RTNL lock. Wait for in-flight readers that may still + * hold a filter pointer and publish it into the fastmap after we + * inserted the new filter (or unlinked fold when replacing). + * Once they are done, flush the stale entries while head is still + * valid. Needed on both the creation and replace paths: on + * creation, readers may have cached a ROUTE4_FAILURE entry for the + * (id, iif) tuple that the new filter now matches. + */ + synchronize_rcu(); route4_reset_fastmap(head); *arg = f; if (fold) { -- 2.34.1