Re: [PATCH net] net/sched: defer qdisc freeing after failed creation

From: David Lee

Date: Mon Aug 10 2026 - 12:23:24 EST


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@xxxxxxxxxxxx> wrote:
On Wed, Aug 5, 2026 at 6:25 AM David Lee <david.lee@xxxxxxxxxxxxxxx> wrote:
>
> From: Kyle Zeng <kylebot@xxxxxxxxxx>
>
> 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@xxxxxxxxxx>
> Signed-off-by: David Lee <david.lee@xxxxxxxxxxxxxxx>

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: poc.c
Description: Binary data