* [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put [not found] ` <CANn89iLFBPHjjOZg5p7b=-vA++17oBLCJtGi8aJXnfJ3j67J1g@mail.gmail.com> @ 2026-07-15 11:41 ` Aldo Ariel Panzardo 2026-07-21 23:46 ` Jamal Hadi Salim 2026-07-22 22:10 ` patchwork-bot+netdevbpf 0 siblings, 2 replies; 4+ messages in thread From: Aldo Ariel Panzardo @ 2026-07-15 11:41 UTC (permalink / raw) To: netdev Cc: Jamal Hadi Salim, Jiri Pirko, Eric Dumazet, David S . Miller, Jakub Kicinski, Paolo Abeni, Simon Horman, stable, linux-kernel, Aldo Ariel Panzardo qdisc_get_rtab() and qdisc_put_rtab() mutate the process-global singly linked list qdisc_rtab_list and a plain non-atomic 'int refcnt' with no lock. This was only safe because every caller historically held the RTNL mutex, which serialized all rate-table lookups, inserts and frees. That invariant no longer holds. cls_flower sets TCF_PROTO_OPS_DOIT_UNLOCKED, so tc_new_tfilter() keeps rtnl_held == false for it and sets TCA_ACT_FLAGS_NO_RTNL. That flag propagates through tcf_exts_validate_ex() -> tcf_action_init() -> tcf_action_init_1() -> tcf_police_init(), which calls qdisc_get_rtab()/qdisc_put_rtab() with the RTNL mutex NOT held. Two RTM_NEWTFILTER requests on different CPUs, each adding a flower filter with a police action carrying the same rate, then race on qdisc_rtab_list and on the non-atomic refcnt, leading to a use-after-free / double-free of the kmalloc-2k struct qdisc_rate_table. qdisc_rtab_list is a single global (not per-netns), so the corrupted object is shared system-wide. BUG: KASAN: slab-use-after-free in qdisc_put_rtab+0x12f/0x160 qdisc_put_rtab+0x12f/0x160 tcf_police_init+0xda9/0x1590 tcf_action_init_1+0x460/0x6b0 tcf_action_init+0x439/0xa40 tcf_exts_validate_ex+0x42d/0x550 fl_change+0xddd/0x7da0 tc_new_tfilter+0xaa7/0x2420 rtnetlink_rcv_msg+0x95e/0xe90 which belongs to the cache kmalloc-2k of size 2048 Protect qdisc_rtab_list and the refcount with a dedicated spinlock. The (sleeping, GFP_KERNEL) allocation in qdisc_get_rtab() is performed before taking the lock; if a concurrent inserter added an identical table in the meantime the freshly allocated one is freed under the lock, so no duplicate is leaked. qdisc_put_rtab() now decrements the refcount and unlinks under the same lock. Fixes: 470502de5bdb ("net: sched: unlock rules update API") Suggested-by: Eric Dumazet <edumazet@google.com> Signed-off-by: Aldo Ariel Panzardo <qwe.aldo@gmail.com> Cc: stable@vger.kernel.org --- v2: - Rework qdisc_get_rtab() to allocate before taking the lock and free the surplus table under the lock instead of dropping the lock to allocate and re-scanning (Eric Dumazet). - Add Fixes: tag and Cc: stable. - Build-tested (CONFIG_NET_SCHED=y). v1 was sent privately to security@kernel.org on 2026-07-11; moving to the public list now that a fix is ready, as requested. net/sched/sch_api.c | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/net/sched/sch_api.c b/net/sched/sch_api.c index 8a3236456db4..668bcd60d183 100644 --- a/net/sched/sch_api.c +++ b/net/sched/sch_api.c @@ -415,12 +415,13 @@ static __u8 __detect_linklayer(struct tc_ratespec *r, __u32 *rtab) } static struct qdisc_rate_table *qdisc_rtab_list; +static DEFINE_SPINLOCK(qdisc_rtab_lock); struct qdisc_rate_table *qdisc_get_rtab(struct tc_ratespec *r, struct nlattr *tab, struct netlink_ext_ack *extack) { - struct qdisc_rate_table *rtab; + struct qdisc_rate_table *rtab, *new_rtab; if (tab == NULL || r->rate == 0 || r->cell_log == 0 || r->cell_log >= 32 || @@ -429,15 +430,20 @@ struct qdisc_rate_table *qdisc_get_rtab(struct tc_ratespec *r, return NULL; } + new_rtab = kmalloc_obj(*new_rtab); + + spin_lock(&qdisc_rtab_lock); for (rtab = qdisc_rtab_list; rtab; rtab = rtab->next) { if (!memcmp(&rtab->rate, r, sizeof(struct tc_ratespec)) && !memcmp(&rtab->data, nla_data(tab), TC_RTAB_SIZE)) { rtab->refcnt++; + spin_unlock(&qdisc_rtab_lock); + kfree(new_rtab); return rtab; } } - rtab = kmalloc_obj(*rtab); + rtab = new_rtab; if (rtab) { rtab->rate = *r; rtab->refcnt = 1; @@ -449,6 +455,7 @@ struct qdisc_rate_table *qdisc_get_rtab(struct tc_ratespec *r, } else { NL_SET_ERR_MSG(extack, "Failed to allocate new qdisc rate table"); } + spin_unlock(&qdisc_rtab_lock); return rtab; } EXPORT_SYMBOL(qdisc_get_rtab); @@ -457,18 +464,25 @@ void qdisc_put_rtab(struct qdisc_rate_table *tab) { struct qdisc_rate_table *rtab, **rtabp; - if (!tab || --tab->refcnt) + if (!tab) return; + spin_lock(&qdisc_rtab_lock); + if (--tab->refcnt) { + spin_unlock(&qdisc_rtab_lock); + return; + } + for (rtabp = &qdisc_rtab_list; (rtab = *rtabp) != NULL; rtabp = &rtab->next) { if (rtab == tab) { *rtabp = rtab->next; - kfree(rtab); - return; + break; } } + spin_unlock(&qdisc_rtab_lock); + kfree(tab); } EXPORT_SYMBOL(qdisc_put_rtab); -- 2.43.0 ^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put 2026-07-15 11:41 ` [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put Aldo Ariel Panzardo @ 2026-07-21 23:46 ` Jamal Hadi Salim 2026-07-22 0:00 ` Eric Dumazet 2026-07-22 22:10 ` patchwork-bot+netdevbpf 1 sibling, 1 reply; 4+ messages in thread From: Jamal Hadi Salim @ 2026-07-21 23:46 UTC (permalink / raw) To: Aldo Ariel Panzardo Cc: netdev, Jiri Pirko, Eric Dumazet, David S . Miller, Jakub Kicinski, Paolo Abeni, Simon Horman, stable, linux-kernel On Wed, Jul 15, 2026 at 7:41 AM Aldo Ariel Panzardo <qwe.aldo@gmail.com> wrote: > > qdisc_get_rtab() and qdisc_put_rtab() mutate the process-global singly > linked list qdisc_rtab_list and a plain non-atomic 'int refcnt' with no > lock. This was only safe because every caller historically held the RTNL > mutex, which serialized all rate-table lookups, inserts and frees. > > That invariant no longer holds. cls_flower sets > TCF_PROTO_OPS_DOIT_UNLOCKED, so tc_new_tfilter() keeps rtnl_held == false > for it and sets TCA_ACT_FLAGS_NO_RTNL. That flag propagates through > tcf_exts_validate_ex() -> tcf_action_init() -> tcf_action_init_1() -> > tcf_police_init(), which calls qdisc_get_rtab()/qdisc_put_rtab() with the > RTNL mutex NOT held. Two RTM_NEWTFILTER requests on different CPUs, each > adding a flower filter with a police action carrying the same rate, then > race on qdisc_rtab_list and on the non-atomic refcnt, leading to a > use-after-free / double-free of the kmalloc-2k struct qdisc_rate_table. > qdisc_rtab_list is a single global (not per-netns), so the corrupted > object is shared system-wide. > > BUG: KASAN: slab-use-after-free in qdisc_put_rtab+0x12f/0x160 > qdisc_put_rtab+0x12f/0x160 > tcf_police_init+0xda9/0x1590 > tcf_action_init_1+0x460/0x6b0 > tcf_action_init+0x439/0xa40 > tcf_exts_validate_ex+0x42d/0x550 > fl_change+0xddd/0x7da0 > tc_new_tfilter+0xaa7/0x2420 > rtnetlink_rcv_msg+0x95e/0xe90 > which belongs to the cache kmalloc-2k of size 2048 > > Protect qdisc_rtab_list and the refcount with a dedicated spinlock. The > (sleeping, GFP_KERNEL) allocation in qdisc_get_rtab() is performed before > taking the lock; if a concurrent inserter added an identical table in the > meantime the freshly allocated one is freed under the lock, so no > duplicate is leaked. qdisc_put_rtab() now decrements the refcount and > unlinks under the same lock. > > Fixes: 470502de5bdb ("net: sched: unlock rules update API") > Suggested-by: Eric Dumazet <edumazet@google.com> > Signed-off-by: Aldo Ariel Panzardo <qwe.aldo@gmail.com> The fix looks reasonable to me. I also tested the poc So: Acked-by: Jamal Hadi Salim <jhs@mojatatu.com> cheers, jamal > Cc: stable@vger.kernel.org > --- > > v2: > - Rework qdisc_get_rtab() to allocate before taking the lock and free the > surplus table under the lock instead of dropping the lock to allocate > and re-scanning (Eric Dumazet). > - Add Fixes: tag and Cc: stable. > - Build-tested (CONFIG_NET_SCHED=y). > > v1 was sent privately to security@kernel.org on 2026-07-11; moving to the > public list now that a fix is ready, as requested. > net/sched/sch_api.c | 24 +++++++++++++++++++----- > 1 file changed, 19 insertions(+), 5 deletions(-) > > diff --git a/net/sched/sch_api.c b/net/sched/sch_api.c > index 8a3236456db4..668bcd60d183 100644 > --- a/net/sched/sch_api.c > +++ b/net/sched/sch_api.c > @@ -415,12 +415,13 @@ static __u8 __detect_linklayer(struct tc_ratespec *r, __u32 *rtab) > } > > static struct qdisc_rate_table *qdisc_rtab_list; > +static DEFINE_SPINLOCK(qdisc_rtab_lock); > > struct qdisc_rate_table *qdisc_get_rtab(struct tc_ratespec *r, > struct nlattr *tab, > struct netlink_ext_ack *extack) > { > - struct qdisc_rate_table *rtab; > + struct qdisc_rate_table *rtab, *new_rtab; > > if (tab == NULL || r->rate == 0 || > r->cell_log == 0 || r->cell_log >= 32 || > @@ -429,15 +430,20 @@ struct qdisc_rate_table *qdisc_get_rtab(struct tc_ratespec *r, > return NULL; > } > > + new_rtab = kmalloc_obj(*new_rtab); > + > + spin_lock(&qdisc_rtab_lock); > for (rtab = qdisc_rtab_list; rtab; rtab = rtab->next) { > if (!memcmp(&rtab->rate, r, sizeof(struct tc_ratespec)) && > !memcmp(&rtab->data, nla_data(tab), TC_RTAB_SIZE)) { > rtab->refcnt++; > + spin_unlock(&qdisc_rtab_lock); > + kfree(new_rtab); > return rtab; > } > } > > - rtab = kmalloc_obj(*rtab); > + rtab = new_rtab; > if (rtab) { > rtab->rate = *r; > rtab->refcnt = 1; > @@ -449,6 +455,7 @@ struct qdisc_rate_table *qdisc_get_rtab(struct tc_ratespec *r, > } else { > NL_SET_ERR_MSG(extack, "Failed to allocate new qdisc rate table"); > } > + spin_unlock(&qdisc_rtab_lock); > return rtab; > } > EXPORT_SYMBOL(qdisc_get_rtab); > @@ -457,18 +464,25 @@ void qdisc_put_rtab(struct qdisc_rate_table *tab) > { > struct qdisc_rate_table *rtab, **rtabp; > > - if (!tab || --tab->refcnt) > + if (!tab) > return; > > + spin_lock(&qdisc_rtab_lock); > + if (--tab->refcnt) { > + spin_unlock(&qdisc_rtab_lock); > + return; > + } > + > for (rtabp = &qdisc_rtab_list; > (rtab = *rtabp) != NULL; > rtabp = &rtab->next) { > if (rtab == tab) { > *rtabp = rtab->next; > - kfree(rtab); > - return; > + break; > } > } > + spin_unlock(&qdisc_rtab_lock); > + kfree(tab); > } > EXPORT_SYMBOL(qdisc_put_rtab); > > -- > 2.43.0 > ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put 2026-07-21 23:46 ` Jamal Hadi Salim @ 2026-07-22 0:00 ` Eric Dumazet 0 siblings, 0 replies; 4+ messages in thread From: Eric Dumazet @ 2026-07-22 0:00 UTC (permalink / raw) To: Jamal Hadi Salim Cc: Aldo Ariel Panzardo, netdev, Jiri Pirko, David S . Miller, Jakub Kicinski, Paolo Abeni, Simon Horman, stable, linux-kernel On Wed, Jul 22, 2026 at 1:47 AM Jamal Hadi Salim <jhs@mojatatu.com> wrote: > > On Wed, Jul 15, 2026 at 7:41 AM Aldo Ariel Panzardo <qwe.aldo@gmail.com> wrote: > > > > qdisc_get_rtab() and qdisc_put_rtab() mutate the process-global singly > > linked list qdisc_rtab_list and a plain non-atomic 'int refcnt' with no > > lock. This was only safe because every caller historically held the RTNL > > mutex, which serialized all rate-table lookups, inserts and frees. > > > > That invariant no longer holds. cls_flower sets > > TCF_PROTO_OPS_DOIT_UNLOCKED, so tc_new_tfilter() keeps rtnl_held == false > > for it and sets TCA_ACT_FLAGS_NO_RTNL. That flag propagates through > > tcf_exts_validate_ex() -> tcf_action_init() -> tcf_action_init_1() -> > > tcf_police_init(), which calls qdisc_get_rtab()/qdisc_put_rtab() with the > > RTNL mutex NOT held. Two RTM_NEWTFILTER requests on different CPUs, each > > adding a flower filter with a police action carrying the same rate, then > > race on qdisc_rtab_list and on the non-atomic refcnt, leading to a > > use-after-free / double-free of the kmalloc-2k struct qdisc_rate_table. > > qdisc_rtab_list is a single global (not per-netns), so the corrupted > > object is shared system-wide. > > > > BUG: KASAN: slab-use-after-free in qdisc_put_rtab+0x12f/0x160 > > qdisc_put_rtab+0x12f/0x160 > > tcf_police_init+0xda9/0x1590 > > tcf_action_init_1+0x460/0x6b0 > > tcf_action_init+0x439/0xa40 > > tcf_exts_validate_ex+0x42d/0x550 > > fl_change+0xddd/0x7da0 > > tc_new_tfilter+0xaa7/0x2420 > > rtnetlink_rcv_msg+0x95e/0xe90 > > which belongs to the cache kmalloc-2k of size 2048 > > > > Protect qdisc_rtab_list and the refcount with a dedicated spinlock. The > > (sleeping, GFP_KERNEL) allocation in qdisc_get_rtab() is performed before > > taking the lock; if a concurrent inserter added an identical table in the > > meantime the freshly allocated one is freed under the lock, so no > > duplicate is leaked. qdisc_put_rtab() now decrements the refcount and > > unlinks under the same lock. > > > > Fixes: 470502de5bdb ("net: sched: unlock rules update API") > > Suggested-by: Eric Dumazet <edumazet@google.com> > > Signed-off-by: Aldo Ariel Panzardo <qwe.aldo@gmail.com> > > The fix looks reasonable to me. I also tested the poc > So: > Acked-by: Jamal Hadi Salim <jhs@mojatatu.com> Same here, thanks! Reviewed-by: Eric Dumazet <edumazet@google.com> ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put 2026-07-15 11:41 ` [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put Aldo Ariel Panzardo 2026-07-21 23:46 ` Jamal Hadi Salim @ 2026-07-22 22:10 ` patchwork-bot+netdevbpf 1 sibling, 0 replies; 4+ messages in thread From: patchwork-bot+netdevbpf @ 2026-07-22 22:10 UTC (permalink / raw) To: Aldo Ariel Panzardo Cc: netdev, jhs, jiri, edumazet, davem, kuba, pabeni, horms, stable, linux-kernel Hello: This patch was applied to netdev/net.git (main) by Jakub Kicinski <kuba@kernel.org>: On Wed, 15 Jul 2026 08:41:14 -0300 you wrote: > qdisc_get_rtab() and qdisc_put_rtab() mutate the process-global singly > linked list qdisc_rtab_list and a plain non-atomic 'int refcnt' with no > lock. This was only safe because every caller historically held the RTNL > mutex, which serialized all rate-table lookups, inserts and frees. > > That invariant no longer holds. cls_flower sets > TCF_PROTO_OPS_DOIT_UNLOCKED, so tc_new_tfilter() keeps rtnl_held == false > for it and sets TCA_ACT_FLAGS_NO_RTNL. That flag propagates through > tcf_exts_validate_ex() -> tcf_action_init() -> tcf_action_init_1() -> > tcf_police_init(), which calls qdisc_get_rtab()/qdisc_put_rtab() with the > RTNL mutex NOT held. Two RTM_NEWTFILTER requests on different CPUs, each > adding a flower filter with a police action carrying the same rate, then > race on qdisc_rtab_list and on the non-atomic refcnt, leading to a > use-after-free / double-free of the kmalloc-2k struct qdisc_rate_table. > qdisc_rtab_list is a single global (not per-netns), so the corrupted > object is shared system-wide. > > [...] Here is the summary with links: - [net,v2] net/sched: serialize qdisc_rtab_list against concurrent get/put https://git.kernel.org/netdev/net/c/f43ee0c0730d You are awesome, thank you! -- Deet-doot-dot, I am a bot. https://korg.docs.kernel.org/patchwork/pwbot.html ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-22 22:10 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <CAP48HfvFnArD5hDW8gCAWrp4Hz8Pbh7m3A8F6DiPtLYq45WOBg@mail.gmail.com>
[not found] ` <CAM0EoMntb24oXpBW-pAYVX1WYTNnTU9eJLe-cvoiD-GGdW-Rkw@mail.gmail.com>
[not found] ` <CANn89iK63bSCL3MPBQKiYEwYfioNYkvTBYVL8cavM2THQbPyhA@mail.gmail.com>
[not found] ` <CANn89i+6K3TrAx0Jq_6Z+OtLBt6DhV6_dNjU5U6m6epucPzVVg@mail.gmail.com>
[not found] ` <CANn89iLFBPHjjOZg5p7b=-vA++17oBLCJtGi8aJXnfJ3j67J1g@mail.gmail.com>
2026-07-15 11:41 ` [PATCH net v2] net/sched: serialize qdisc_rtab_list against concurrent get/put Aldo Ariel Panzardo
2026-07-21 23:46 ` Jamal Hadi Salim
2026-07-22 0:00 ` Eric Dumazet
2026-07-22 22:10 ` patchwork-bot+netdevbpf
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.