KASAN: slab-use-after-free Write in mini_qdisc_pair_swap
Subsystems (3)
syzbot
syzbotHello,
syzbot found the following issue on:
HEAD commit: fff5a5e7f528 Merge tag 'for-linus' of git://git.armlinux.o.. git tree: upstream console output: https://syzkaller.appspot.com/x/log.txt?x=11884731c80000 kernel config: https://syzkaller.appspot.com/x/.config?x=aaa4b45720ca0519 dashboard link: https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b compiler: gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2 syz repro: https://syzkaller.appspot.com/x/repro.syz?x=15d1497ac80000 C reproducer: https://syzkaller.appspot.com/x/repro.c?x=11eed636c80000
Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/33a184f98b9d/disk-fff5a5e7.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/3d75f967571e/vmlinux-fff5a5e7.xz
kernel image: https://storage.googleapis.com/syzbot-assets/4eeb8edbdc7e/bzImage-fff5a5e7.xz
IMPORTANT: if you fix the issue, please add the following tag to the commit:
==================================================================
BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 CPU: 0 PID: 14901 Comm: syz-executor690 Not tainted 6.3.0-rc3-syzkaller-00026-gfff5a5e7f528 #0 Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 03/02/2023 Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd RIP: 0033:0x7f4f11222579 Code: 28 00 00 00 75 05 48 83 c4 28 c3 e8 31 19 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 RSP: 002b:00007f4f11184208 EFLAGS: 00000246 ORIG_RAX: 0000000000000133 RAX: ffffffffffffffda RBX: 00007f4f112ab2a8 RCX: 00007f4f11222579 RDX: 040000000000009f RSI: 00000000200002c0 RDI: 0000000000000007 RBP: 00007f4f112ab2a0 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 00007f4f112ab2ac R13: 00007fffc8e5214f R14: 00007f4f11184300 R15: 0000000000022000 </TASK> Allocated by task 14898: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 kasan_set_track+0x25/0x30 mm/kasan/common.c:52 ____kasan_kmalloc mm/kasan/common.c:374 [inline] ____kasan_kmalloc mm/kasan/common.c:333 [inline] __kasan_kmalloc+0xa2/0xb0 mm/kasan/common.c:383 kasan_kmalloc include/linux/kasan.h:196 [inline] __do_kmalloc_node mm/slab_common.c:967 [inline] __kmalloc_node+0x61/0x1a0 mm/slab_common.c:974 kmalloc_node include/linux/slab.h:610 [inline] kzalloc_node include/linux/slab.h:731 [inline] qdisc_alloc+0xb0/0xb30 net/sched/sch_generic.c:938 qdisc_create+0xce/0x1040 net/sched/sch_api.c:1244 tc_modify_qdisc+0x488/0x1a40 net/sched/sch_api.c:1680 rtnetlink_rcv_msg+0x43d/0xd50 net/core/rtnetlink.c:6174 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd Freed by task 21: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 kasan_set_track+0x25/0x30 mm/kasan/common.c:52 kasan_save_free_info+0x2e/0x40 mm/kasan/generic.c:521 ____kasan_slab_free mm/kasan/common.c:236 [inline] ____kasan_slab_free+0x160/0x1c0 mm/kasan/common.c:200 kasan_slab_free include/linux/kasan.h:162 [inline] slab_free_hook mm/slub.c:1781 [inline] slab_free_freelist_hook+0x8b/0x1c0 mm/slub.c:1807 slab_free mm/slub.c:3787 [inline] __kmem_cache_free+0xaf/0x2d0 mm/slub.c:3800 rcu_do_batch kernel/rcu/tree.c:2112 [inline] rcu_core+0x814/0x1960 kernel/rcu/tree.c:2372 __do_softirq+0x1d4/0x905 kernel/softirq.c:571 Last potentially related work creation: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 __kasan_record_aux_stack+0xbc/0xd0 mm/kasan/generic.c:491 __call_rcu_common.constprop.0+0x99/0x7e0 kernel/rcu/tree.c:2622 qdisc_put_unlocked+0x73/0x90 net/sched/sch_generic.c:1097 tcf_block_release+0x86/0x90 net/sched/cls_api.c:1362 tc_new_tfilter+0xa35/0x2290 net/sched/cls_api.c:2331 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd The buggy address belongs to the object at ffff888045b31000 which belongs to the cache kmalloc-1k of size 1024
The buggy address is located 776 bytes inside of
freed 1024-byte region [ffff888045b31000, ffff888045b31400)
The buggy address belongs to the physical page:
page:ffffea000116cc00 refcount:1 mapcount:0 mapping:0000000000000000 index:0xffff888045b37000 pfn:0x45b30
head:ffffea000116cc00 order:3 entire_mapcount:0 nr_pages_mapped:0 pincount:0
anon flags: 0xfff00000010200(slab|head|node=0|zone=1|lastcpupid=0x7ff)
raw: 00fff00000010200 ffff888012441dc0 0000000000000000 dead000000000001
raw: ffff888045b37000 000000008010000d 00000001ffffffff 0000000000000000
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 3, migratetype Unmovable, gfp_mask 0x1d20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC|__GFP_HARDWALL), pid 14184, tgid 14168 (syz-executor690), ts 420322459064, free_ts 15493742100
prep_new_page mm/page_alloc.c:2552 [inline]
get_page_from_freelist+0x1190/0x2e20 mm/page_alloc.c:4325
__alloc_pages+0x1cb/0x4a0 mm/page_alloc.c:5591
alloc_pages+0x1aa/0x270 mm/mempolicy.c:2283
alloc_slab_page mm/slub.c:1851 [inline]
allocate_slab+0x25f/0x390 mm/slub.c:1998
new_slab mm/slub.c:2051 [inline]
___slab_alloc+0xa91/0x1400 mm/slub.c:3193
__slab_alloc.constprop.0+0x56/0xa0 mm/slub.c:3292
__slab_alloc_node mm/slub.c:3345 [inline]
slab_alloc_node mm/slub.c:3442 [inline]
__kmem_cache_alloc_node+0x136/0x320 mm/slub.c:3491
kmalloc_trace+0x26/0xe0 mm/slab_common.c:1061
kmalloc include/linux/slab.h:580 [inline]
kmalloc_array include/linux/slab.h:635 [inline]
kcalloc include/linux/slab.h:667 [inline]
fl_change+0x1cf/0x4ac0 net/sched/cls_flower.c:2175
tc_new_tfilter+0x97c/0x2290 net/sched/cls_api.c:2310
rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165
netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574
netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline]
netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365
netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942
sock_sendmsg_nosec net/socket.c:724 [inline]
sock_sendmsg+0xde/0x190 net/socket.c:747
____sys_sendmsg+0x334/0x900 net/socket.c:2501
page last free stack trace:
reset_page_owner include/linux/page_owner.h:24 [inline]
free_pages_prepare mm/page_alloc.c:1453 [inline]
free_pcp_prepare+0x5d5/0xa50 mm/page_alloc.c:1503
free_unref_page_prepare mm/page_alloc.c:3387 [inline]
free_unref_page+0x1d/0x490 mm/page_alloc.c:3482
free_contig_range+0xb5/0x180 mm/page_alloc.c:9531
destroy_args+0x6c4/0x920 mm/debug_vm_pgtable.c:1023
debug_vm_pgtable+0x242a/0x4640 mm/debug_vm_pgtable.c:1403
do_one_initcall+0x102/0x540 init/main.c:1310
do_initcall_level init/main.c:1383 [inline]
do_initcalls init/main.c:1399 [inline]
do_basic_setup init/main.c:1418 [inline]
kernel_init_freeable+0x696/0xc00 init/main.c:1638
kernel_init+0x1e/0x2c0 init/main.c:1526
ret_from_fork+0x1f/0x30 arch/x86/entry/entry_64.S:308
Memory state around the buggy address:
ffff888045b31200: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff888045b31280: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
>ffff888045b31300: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
^
ffff888045b31380: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff888045b31400: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
==================================================================---
This report is generated by a bot. It may contain errors. See https://goo.gl/tpsmEJ for more information about syzbot. syzbot engineers can be reached at [email protected].
syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot. syzbot can test patches for this issue, for details see:
https://goo.gl/tpsmEJ#testing-patches
Jakub Kicinski
maintainerSeth, does this looks related to commit 267463823adb ("net: sch:
eliminate unnecessary RCU waits in mini_qdisc_pair_swap()") by any chance?
Quoted 212 lines · On Thu, 23 Mar 2023 17:52:40 -0700 syzbot wrote:
Hello,
syzbot found the following issue on:
HEAD commit: fff5a5e7f528 Merge tag 'for-linus' of git://git.armlinux.o.. git tree: upstream console output: https://syzkaller.appspot.com/x/log.txt?x=11884731c80000 kernel config: https://syzkaller.appspot.com/x/.config?x=aaa4b45720ca0519 dashboard link: https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b compiler: gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2 syz repro: https://syzkaller.appspot.com/x/repro.syz?x=15d1497ac80000 C reproducer: https://syzkaller.appspot.com/x/repro.c?x=11eed636c80000Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/33a184f98b9d/disk-fff5a5e7.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/3d75f967571e/vmlinux-fff5a5e7.xz
kernel image: https://storage.googleapis.com/syzbot-assets/4eeb8edbdc7e/bzImage-fff5a5e7.xzIMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by [email protected]==================================================================
BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 CPU: 0 PID: 14901 Comm: syz-executor690 Not tainted 6.3.0-rc3-syzkaller-00026-gfff5a5e7f528 #0 Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 03/02/2023 Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd RIP: 0033:0x7f4f11222579 Code: 28 00 00 00 75 05 48 83 c4 28 c3 e8 31 19 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 RSP: 002b:00007f4f11184208 EFLAGS: 00000246 ORIG_RAX: 0000000000000133 RAX: ffffffffffffffda RBX: 00007f4f112ab2a8 RCX: 00007f4f11222579 RDX: 040000000000009f RSI: 00000000200002c0 RDI: 0000000000000007 RBP: 00007f4f112ab2a0 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 00007f4f112ab2ac R13: 00007fffc8e5214f R14: 00007f4f11184300 R15: 0000000000022000 </TASK> Allocated by task 14898: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 kasan_set_track+0x25/0x30 mm/kasan/common.c:52 ____kasan_kmalloc mm/kasan/common.c:374 [inline] ____kasan_kmalloc mm/kasan/common.c:333 [inline] __kasan_kmalloc+0xa2/0xb0 mm/kasan/common.c:383 kasan_kmalloc include/linux/kasan.h:196 [inline] __do_kmalloc_node mm/slab_common.c:967 [inline] __kmalloc_node+0x61/0x1a0 mm/slab_common.c:974 kmalloc_node include/linux/slab.h:610 [inline] kzalloc_node include/linux/slab.h:731 [inline] qdisc_alloc+0xb0/0xb30 net/sched/sch_generic.c:938 qdisc_create+0xce/0x1040 net/sched/sch_api.c:1244 tc_modify_qdisc+0x488/0x1a40 net/sched/sch_api.c:1680 rtnetlink_rcv_msg+0x43d/0xd50 net/core/rtnetlink.c:6174 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd Freed by task 21: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 kasan_set_track+0x25/0x30 mm/kasan/common.c:52 kasan_save_free_info+0x2e/0x40 mm/kasan/generic.c:521 ____kasan_slab_free mm/kasan/common.c:236 [inline] ____kasan_slab_free+0x160/0x1c0 mm/kasan/common.c:200 kasan_slab_free include/linux/kasan.h:162 [inline] slab_free_hook mm/slub.c:1781 [inline] slab_free_freelist_hook+0x8b/0x1c0 mm/slub.c:1807 slab_free mm/slub.c:3787 [inline] __kmem_cache_free+0xaf/0x2d0 mm/slub.c:3800 rcu_do_batch kernel/rcu/tree.c:2112 [inline] rcu_core+0x814/0x1960 kernel/rcu/tree.c:2372 __do_softirq+0x1d4/0x905 kernel/softirq.c:571 Last potentially related work creation: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 __kasan_record_aux_stack+0xbc/0xd0 mm/kasan/generic.c:491 __call_rcu_common.constprop.0+0x99/0x7e0 kernel/rcu/tree.c:2622 qdisc_put_unlocked+0x73/0x90 net/sched/sch_generic.c:1097 tcf_block_release+0x86/0x90 net/sched/cls_api.c:1362 tc_new_tfilter+0xa35/0x2290 net/sched/cls_api.c:2331 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd The buggy address belongs to the object at ffff888045b31000 which belongs to the cache kmalloc-1k of size 1024The buggy address is located 776 bytes inside of
freed 1024-byte region [ffff888045b31000,ffff888045b31400)The buggy address belongs to the physical page: page:ffffea000116cc00 refcount:1 mapcount:0 mapping:0000000000000000 index:0xffff888045b37000 pfn:0x45b30 head:ffffea000116cc00 order:3 entire_mapcount:0 nr_pages_mapped:0 pincount:0 anon flags: 0xfff00000010200(slab|head|node=0|zone=1|lastcpupid=0x7ff) raw: 00fff00000010200 ffff888012441dc0 0000000000000000 dead000000000001 raw: ffff888045b37000 000000008010000d 00000001ffffffff 0000000000000000 page dumped because: kasan: bad access detected page_owner tracks the page as allocated page last allocated via order 3, migratetype Unmovable, gfp_mask 0x1d20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC|__GFP_HARDWALL), pid 14184, tgid 14168 (syz-executor690), ts 420322459064, free_ts 15493742100 prep_new_page mm/page_alloc.c:2552 [inline] get_page_from_freelist+0x1190/0x2e20 mm/page_alloc.c:4325 __alloc_pages+0x1cb/0x4a0 mm/page_alloc.c:5591 alloc_pages+0x1aa/0x270 mm/mempolicy.c:2283 alloc_slab_page mm/slub.c:1851 [inline] allocate_slab+0x25f/0x390 mm/slub.c:1998 new_slab mm/slub.c:2051 [inline] ___slab_alloc+0xa91/0x1400 mm/slub.c:3193 __slab_alloc.constprop.0+0x56/0xa0 mm/slub.c:3292 __slab_alloc_node mm/slub.c:3345 [inline] slab_alloc_node mm/slub.c:3442 [inline] __kmem_cache_alloc_node+0x136/0x320 mm/slub.c:3491 kmalloc_trace+0x26/0xe0 mm/slab_common.c:1061 kmalloc include/linux/slab.h:580 [inline] kmalloc_array include/linux/slab.h:635 [inline] kcalloc include/linux/slab.h:667 [inline] fl_change+0x1cf/0x4ac0 net/sched/cls_flower.c:2175 tc_new_tfilter+0x97c/0x2290 net/sched/cls_api.c:2310 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 page last free stack trace: reset_page_owner include/linux/page_owner.h:24 [inline] free_pages_prepare mm/page_alloc.c:1453 [inline] free_pcp_prepare+0x5d5/0xa50 mm/page_alloc.c:1503 free_unref_page_prepare mm/page_alloc.c:3387 [inline] free_unref_page+0x1d/0x490 mm/page_alloc.c:3482 free_contig_range+0xb5/0x180 mm/page_alloc.c:9531 destroy_args+0x6c4/0x920 mm/debug_vm_pgtable.c:1023 debug_vm_pgtable+0x242a/0x4640 mm/debug_vm_pgtable.c:1403 do_one_initcall+0x102/0x540 init/main.c:1310 do_initcall_level init/main.c:1383 [inline] do_initcalls init/main.c:1399 [inline] do_basic_setup init/main.c:1418 [inline] kernel_init_freeable+0x696/0xc00 init/main.c:1638 kernel_init+0x1e/0x2c0 init/main.c:1526 ret_from_fork+0x1f/0x30 arch/x86/entry/entry_64.S:308 Memory state around the buggy address: ffff888045b31200: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ffff888045b31280: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb >ffff888045b31300: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ^ ffff888045b31380: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ffff888045b31400: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc ==================================================================---
This report is generated by a bot. It may contain errors. See https://goo.gl/tpsmEJ for more information about syzbot. syzbot engineers can be reached at [email protected].syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot. syzbot can test patches for this issue, for details see:
https://goo.gl/tpsmEJ#testing-patches
Quoted 3 lines · Jakub Kicinski
Seth, does this looks related to commit
267463823adb("net: sch:
eliminate unnecessary RCU waits inmini_qdisc_pair_swap()") by any chance?
I don't see how it could be. The memory being written is part of the qdisc private memory, and tc_new_tfilter() takes a reference to the qdisc. If that memory has been freed doesn't it mean that something has done an unbalanced qdisc_put()?
Quoted 214 lines · On Thu, 23 Mar 2023 17:52:40 -0700 syzbot wrote:
Quoted 212 lines
Hello,
syzbot found the following issue on:
HEAD commit: fff5a5e7f528 Merge tag 'for-linus' of git://git.armlinux.o.. git tree: upstream console output: https://syzkaller.appspot.com/x/log.txt?x=11884731c80000 kernel config: https://syzkaller.appspot.com/x/.config?x=aaa4b45720ca0519 dashboard link: https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b compiler: gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2 syz repro: https://syzkaller.appspot.com/x/repro.syz?x=15d1497ac80000 C reproducer: https://syzkaller.appspot.com/x/repro.c?x=11eed636c80000Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/33a184f98b9d/disk-fff5a5e7.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/3d75f967571e/vmlinux-fff5a5e7.xz
kernel image: https://storage.googleapis.com/syzbot-assets/4eeb8edbdc7e/bzImage-fff5a5e7.xzIMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by [email protected]==================================================================
BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 CPU: 0 PID: 14901 Comm: syz-executor690 Not tainted 6.3.0-rc3-syzkaller-00026-gfff5a5e7f528 #0 Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 03/02/2023 Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd RIP: 0033:0x7f4f11222579 Code: 28 00 00 00 75 05 48 83 c4 28 c3 e8 31 19 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 RSP: 002b:00007f4f11184208 EFLAGS: 00000246 ORIG_RAX: 0000000000000133 RAX: ffffffffffffffda RBX: 00007f4f112ab2a8 RCX: 00007f4f11222579 RDX: 040000000000009f RSI: 00000000200002c0 RDI: 0000000000000007 RBP: 00007f4f112ab2a0 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 00007f4f112ab2ac R13: 00007fffc8e5214f R14: 00007f4f11184300 R15: 0000000000022000 </TASK> Allocated by task 14898: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 kasan_set_track+0x25/0x30 mm/kasan/common.c:52 ____kasan_kmalloc mm/kasan/common.c:374 [inline] ____kasan_kmalloc mm/kasan/common.c:333 [inline] __kasan_kmalloc+0xa2/0xb0 mm/kasan/common.c:383 kasan_kmalloc include/linux/kasan.h:196 [inline] __do_kmalloc_node mm/slab_common.c:967 [inline] __kmalloc_node+0x61/0x1a0 mm/slab_common.c:974 kmalloc_node include/linux/slab.h:610 [inline] kzalloc_node include/linux/slab.h:731 [inline] qdisc_alloc+0xb0/0xb30 net/sched/sch_generic.c:938 qdisc_create+0xce/0x1040 net/sched/sch_api.c:1244 tc_modify_qdisc+0x488/0x1a40 net/sched/sch_api.c:1680 rtnetlink_rcv_msg+0x43d/0xd50 net/core/rtnetlink.c:6174 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd Freed by task 21: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 kasan_set_track+0x25/0x30 mm/kasan/common.c:52 kasan_save_free_info+0x2e/0x40 mm/kasan/generic.c:521 ____kasan_slab_free mm/kasan/common.c:236 [inline] ____kasan_slab_free+0x160/0x1c0 mm/kasan/common.c:200 kasan_slab_free include/linux/kasan.h:162 [inline] slab_free_hook mm/slub.c:1781 [inline] slab_free_freelist_hook+0x8b/0x1c0 mm/slub.c:1807 slab_free mm/slub.c:3787 [inline] __kmem_cache_free+0xaf/0x2d0 mm/slub.c:3800 rcu_do_batch kernel/rcu/tree.c:2112 [inline] rcu_core+0x814/0x1960 kernel/rcu/tree.c:2372 __do_softirq+0x1d4/0x905 kernel/softirq.c:571 Last potentially related work creation: kasan_save_stack+0x22/0x40 mm/kasan/common.c:45 __kasan_record_aux_stack+0xbc/0xd0 mm/kasan/generic.c:491 __call_rcu_common.constprop.0+0x99/0x7e0 kernel/rcu/tree.c:2622 qdisc_put_unlocked+0x73/0x90 net/sched/sch_generic.c:1097 tcf_block_release+0x86/0x90 net/sched/cls_api.c:1362 tc_new_tfilter+0xa35/0x2290 net/sched/cls_api.c:2331 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 ___sys_sendmsg+0x110/0x1b0 net/socket.c:2555 __sys_sendmmsg+0x18f/0x460 net/socket.c:2641 __do_sys_sendmmsg net/socket.c:2670 [inline] __se_sys_sendmmsg net/socket.c:2667 [inline] __x64_sys_sendmmsg+0x9d/0x100 net/socket.c:2667 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x39/0xb0 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x63/0xcd The buggy address belongs to the object at ffff888045b31000 which belongs to the cache kmalloc-1k of size 1024The buggy address is located 776 bytes inside of
freed 1024-byte region [ffff888045b31000,ffff888045b31400)The buggy address belongs to the physical page: page:ffffea000116cc00 refcount:1 mapcount:0 mapping:0000000000000000 index:0xffff888045b37000 pfn:0x45b30 head:ffffea000116cc00 order:3 entire_mapcount:0 nr_pages_mapped:0 pincount:0 anon flags: 0xfff00000010200(slab|head|node=0|zone=1|lastcpupid=0x7ff) raw: 00fff00000010200 ffff888012441dc0 0000000000000000 dead000000000001 raw: ffff888045b37000 000000008010000d 00000001ffffffff 0000000000000000 page dumped because: kasan: bad access detected page_owner tracks the page as allocated page last allocated via order 3, migratetype Unmovable, gfp_mask 0x1d20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC|__GFP_HARDWALL), pid 14184, tgid 14168 (syz-executor690), ts 420322459064, free_ts 15493742100 prep_new_page mm/page_alloc.c:2552 [inline] get_page_from_freelist+0x1190/0x2e20 mm/page_alloc.c:4325 __alloc_pages+0x1cb/0x4a0 mm/page_alloc.c:5591 alloc_pages+0x1aa/0x270 mm/mempolicy.c:2283 alloc_slab_page mm/slub.c:1851 [inline] allocate_slab+0x25f/0x390 mm/slub.c:1998 new_slab mm/slub.c:2051 [inline] ___slab_alloc+0xa91/0x1400 mm/slub.c:3193 __slab_alloc.constprop.0+0x56/0xa0 mm/slub.c:3292 __slab_alloc_node mm/slub.c:3345 [inline] slab_alloc_node mm/slub.c:3442 [inline] __kmem_cache_alloc_node+0x136/0x320 mm/slub.c:3491 kmalloc_trace+0x26/0xe0 mm/slab_common.c:1061 kmalloc include/linux/slab.h:580 [inline] kmalloc_array include/linux/slab.h:635 [inline] kcalloc include/linux/slab.h:667 [inline] fl_change+0x1cf/0x4ac0 net/sched/cls_flower.c:2175 tc_new_tfilter+0x97c/0x2290 net/sched/cls_api.c:2310 rtnetlink_rcv_msg+0x996/0xd50 net/core/rtnetlink.c:6165 netlink_rcv_skb+0x165/0x440 net/netlink/af_netlink.c:2574 netlink_unicast_kernel net/netlink/af_netlink.c:1339 [inline] netlink_unicast+0x547/0x7f0 net/netlink/af_netlink.c:1365 netlink_sendmsg+0x925/0xe30 net/netlink/af_netlink.c:1942 sock_sendmsg_nosec net/socket.c:724 [inline] sock_sendmsg+0xde/0x190 net/socket.c:747 ____sys_sendmsg+0x334/0x900 net/socket.c:2501 page last free stack trace: reset_page_owner include/linux/page_owner.h:24 [inline] free_pages_prepare mm/page_alloc.c:1453 [inline] free_pcp_prepare+0x5d5/0xa50 mm/page_alloc.c:1503 free_unref_page_prepare mm/page_alloc.c:3387 [inline] free_unref_page+0x1d/0x490 mm/page_alloc.c:3482 free_contig_range+0xb5/0x180 mm/page_alloc.c:9531 destroy_args+0x6c4/0x920 mm/debug_vm_pgtable.c:1023 debug_vm_pgtable+0x242a/0x4640 mm/debug_vm_pgtable.c:1403 do_one_initcall+0x102/0x540 init/main.c:1310 do_initcall_level init/main.c:1383 [inline] do_initcalls init/main.c:1399 [inline] do_basic_setup init/main.c:1418 [inline] kernel_init_freeable+0x696/0xc00 init/main.c:1638 kernel_init+0x1e/0x2c0 init/main.c:1526 ret_from_fork+0x1f/0x30 arch/x86/entry/entry_64.S:308 Memory state around the buggy address: ffff888045b31200: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ffff888045b31280: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb >ffff888045b31300: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ^ ffff888045b31380: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb ffff888045b31400: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc ==================================================================---
This report is generated by a bot. It may contain errors. See https://goo.gl/tpsmEJ for more information about syzbot. syzbot engineers can be reached at [email protected].syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot. syzbot can test patches for this issue, for details see:
https://goo.gl/tpsmEJ#testing-patches
Quoted 10 lines · Seth Forshee
Quoted 3 lines · Jakub Kicinski
Seth, does this looks related to commit
267463823adb("net: sch:
eliminate unnecessary RCU waits inmini_qdisc_pair_swap()") by any chance?I don't see how it could be. The memory being written is part of the qdisc private memory, and
tc_new_tfilter()takes a reference to the qdisc. If that memory has been freed doesn't it mean that something has done an unbalancedqdisc_put()?
Reverting Seth's patches (85c0c3eb9a66 and 267463823adb) leads to these traces with the reproducer:
[ 52.704956][ C0] ------------[ cut here ]------------ [ 52.705568][ C0] ODEBUG: free active (active state 1) object:0 [ 52.706542][ C0] WARNING: CPU: 0 PID: 0 at lib/debugobjects.c0 [ 52.707283][ C0] Modules linked in: [ 52.707602][ C0] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 6.0 [ 52.708304][ C0] Hardware name: QEMU Standard PC (i440FX + PI4 [ 52.709032][ C0] RIP: 0010:debug_print_object+0x196/0x290 [ 52.709509][ C0] Code: 48 89 fa 48 c1 ea 03 80 3c 02 00 0f 85e [ 52.711011][ C0] RSP: 0018:ffffc90000007cd0 EFLAGS: 00010282 [ 52.711510][ C0] RAX: 0000000000000000 RBX: 0000000000000003 0 [ 52.712125][ C0] RDX: ffffffff8c495800 RSI: ffffffff814b96d7 1 [ 52.712748][ C0] RBP: 0000000000000001 R08: 0000000000000001 0 [ 52.713370][ C0] R10: 0000000000000000 R11: 203a47554245444f 0 [ 52.713983][ C0] R13: ffffffff8aa6e960 R14: 0000000000000000 8 [ 52.714609][ C0] FS: 0000000000000000(0000) GS:ffff8881f5a000 [ 52.715356][ C0] CS: 0010 DS: 0000 ES: 0000 CR0: 000000008003 [ 52.715863][ C0] CR2: 000055914686f020 CR3: 000000011e856000 0 [ 52.716486][ C0] Call Trace: [ 52.716742][ C0] <IRQ> [ 52.716969][ C0] debug_check_no_obj_freed+0x302/0x420 [ 52.717423][ C0] slab_free_freelist_hook+0xec/0x1c0 [ 52.717848][ C0] ? rcu_core+0x818/0x1930 [ 52.718204][ C0] __kmem_cache_free+0xaf/0x2e0 [ 52.718590][ C0] rcu_core+0x818/0x1930 [ 52.718938][ C0] ? rcu_report_dead+0x610/0x610 [ 52.719328][ C0] __do_softirq+0x1d4/0x8ef [ 52.719689][ C0] __irq_exit_rcu+0x11d/0x190 [ 52.720062][ C0] irq_exit_rcu+0x9/0x20 [ 52.720402][ C0] sysvec_apic_timer_interrupt+0x97/0xc0 [ 52.720842][ C0] </IRQ> [ 52.721070][ C0] <TASK> [ 52.721300][ C0] asm_sysvec_apic_timer_interrupt+0x1a/0x20 [ 52.721779][ C0] RIP: 0010:default_idle+0xf/0x20 [ 52.722172][ C0] Code: 89 07 49 c7 c0 08 00 00 00 4d 29 c8 4c5 [ 52.723631][ C0] RSP: 0018:ffffffff8c407e30 EFLAGS: 00000202 [ 52.724096][ C0] RAX: 000000000007897f RBX: 0000000000000000 6 [ 52.724702][ C0] RDX: 0000000000000000 RSI: 0000000000000001 0 [ 52.725335][ C0] RBP: ffffffff8c495800 R08: 0000000000000001 b [ 52.725957][ C0] R10: ffffed103eb46d95 R11: 0000000000000000 0 [ 52.726550][ C0] R13: 0000000000000000 R14: ffffffff8e7834d0 0 [ 52.727162][ C0] ? ct_kernel_exit+0x1d6/0x240 [ 52.727542][ C0] default_idle_call+0x67/0xa0 [ 52.727912][ C0] do_idle+0x31e/0x3e0 [ 52.728241][ C0] ? arch_cpu_idle_exit+0x30/0x30 [ 52.728635][ C0] cpu_startup_entry+0x18/0x20 [ 52.729006][ C0] rest_init+0x16d/0x2b0 [ 52.729338][ C0] ? regulator_has_full_constraints+0x9/0x20 [ 52.729815][ C0] ? trace_init_perf_perm_irq_work_exit+0x20/00 [ 52.730309][ C0] arch_call_rest_init+0x13/0x30 [ 52.730703][ C0] start_kernel+0x352/0x4c0 [ 52.731087][ C0] secondary_startup_64_no_verify+0xce/0xdb [ 52.731611][ C0] </TASK> [ 52.731870][ C0] Kernel panic - not syncing: kernel: panic_on. [ 52.732445][ C0] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 6.0 [ 52.733140][ C0] Hardware name: QEMU Standard PC (i440FX + PI4 [ 52.733867][ C0] Call Trace: [ 52.734143][ C0] <IRQ> [ 52.734380][ C0] dump_stack_lvl+0xd9/0x150 [ 52.734769][ C0] panic+0x684/0x730 [ 52.735082][ C0] ? panic_smp_self_stop+0x90/0x90 [ 52.735322][ C0] ? show_trace_log_lvl+0x285/0x390 [ 52.735322][ C0] ? debug_print_object+0x196/0x290 [ 52.735322][ C0] check_panic_on_warn+0xb1/0xc0 [ 52.735322][ C0] __warn+0xf2/0x390 [ 52.735322][ C0] ? debug_print_object+0x196/0x290 [ 52.735322][ C0] report_bug+0x2dd/0x500 [ 52.735322][ C0] handle_bug+0x3c/0x70 [ 52.735322][ C0] exc_invalid_op+0x18/0x50 [ 52.735322][ C0] asm_exc_invalid_op+0x1a/0x20 [ 52.735322][ C0] RIP: 0010:debug_print_object+0x196/0x290 [ 52.735322][ C0] Code: 48 89 fa 48 c1 ea 03 80 3c 02 00 0f 85e [ 52.735322][ C0] RSP: 0018:ffffc90000007cd0 EFLAGS: 00010282 [ 52.735322][ C0] RAX: 0000000000000000 RBX: 0000000000000003 0 [ 52.735322][ C0] RDX: ffffffff8c495800 RSI: ffffffff814b96d7 1 [ 52.735322][ C0] RBP: 0000000000000001 R08: 0000000000000001 0 [ 52.735322][ C0] R10: 0000000000000000 R11: 203a47554245444f 0 [ 52.735322][ C0] R13: ffffffff8aa6e960 R14: 0000000000000000 8 [ 52.735322][ C0] ? __warn_printk+0x187/0x310 [ 52.735322][ C0] debug_check_no_obj_freed+0x302/0x420 [ 52.735322][ C0] slab_free_freelist_hook+0xec/0x1c0 [ 52.735322][ C0] ? rcu_core+0x818/0x1930 [ 52.735322][ C0] __kmem_cache_free+0xaf/0x2e0 [ 52.735322][ C0] rcu_core+0x818/0x1930 [ 52.735322][ C0] ? rcu_report_dead+0x610/0x610 [ 52.735322][ C0] __do_softirq+0x1d4/0x8ef [ 52.735322][ C0] __irq_exit_rcu+0x11d/0x190 [ 52.735322][ C0] irq_exit_rcu+0x9/0x20 [ 52.735322][ C0] sysvec_apic_timer_interrupt+0x97/0xc0 [ 52.735322][ C0] </IRQ> [ 52.735322][ C0] <TASK> [ 52.735322][ C0] asm_sysvec_apic_timer_interrupt+0x1a/0x20 [ 52.735322][ C0] RIP: 0010:default_idle+0xf/0x20 [ 52.735322][ C0] Code: 89 07 49 c7 c0 08 00 00 00 4d 29 c8 4c5 [ 52.735322][ C0] RSP: 0018:ffffffff8c407e30 EFLAGS: 00000202 [ 52.735322][ C0] RAX: 000000000007897f RBX: 0000000000000000 6 [ 52.735322][ C0] RDX: 0000000000000000 RSI: 0000000000000001 0 [ 52.735322][ C0] RBP: ffffffff8c495800 R08: 0000000000000001 b [ 52.735322][ C0] R10: ffffed103eb46d95 R11: 0000000000000000 0 [ 52.735322][ C0] R13: 0000000000000000 R14: ffffffff8e7834d0 0 [ 52.735322][ C0] ? ct_kernel_exit+0x1d6/0x240 [ 52.735322][ C0] default_idle_call+0x67/0xa0 [ 52.735322][ C0] do_idle+0x31e/0x3e0 [ 52.735322][ C0] ? arch_cpu_idle_exit+0x30/0x30 [ 52.735322][ C0] cpu_startup_entry+0x18/0x20 [ 52.735322][ C0] rest_init+0x16d/0x2b0 [ 52.735322][ C0] ? regulator_has_full_constraints+0x9/0x20 [ 52.735322][ C0] ? trace_init_perf_perm_irq_work_exit+0x20/00 [ 52.735322][ C0] arch_call_rest_init+0x13/0x30 [ 52.735322][ C0] start_kernel+0x352/0x4c0 [ 52.735322][ C0] secondary_startup_64_no_verify+0xce/0xdb [ 52.735322][ C0] </TASK> [ 52.735322][ C0] Kernel Offset: disabled [ 52.735322][ C0] Rebooting in 86400 seconds..
Jamal Hadi Salim
maintainerTo provide more update:
Happens on single processor before Seth's patches; and only on multi-processor after Seth's patches.
Theory is: there is a logic bug in the miniqdisc rcu visibility. Feels like the freeing of the structure is done without rcu involvement. Jiri/Cong maybe you can take a look since youve been dabbling in miniqdisc? The reproducer worked for me and Pedro 100% of the time.
cheers,
jamal
Quoted 130 lines · Pedro Tammela
Quoted 10 lines · Seth Forshee
Quoted 3 lines · Jakub Kicinski
Seth, does this looks related to commit
267463823adb("net: sch:
eliminate unnecessary RCU waits inmini_qdisc_pair_swap()") by any chance?I don't see how it could be. The memory being written is part of the qdisc private memory, and
tc_new_tfilter()takes a reference to the qdisc. If that memory has been freed doesn't it mean that something has done an unbalancedqdisc_put()?Reverting Seth's patches (
85c0c3eb9a66and267463823adb) leads to these traces with the reproducer:[ 52.704956][ C0] ------------[ cut here ]------------ [ 52.705568][ C0] ODEBUG: free active (active state 1) object:0 [ 52.706542][ C0] WARNING: CPU: 0 PID: 0 at lib/debugobjects.c0 [ 52.707283][ C0] Modules linked in: [ 52.707602][ C0] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 6.0 [ 52.708304][ C0] Hardware name: QEMU Standard PC (i440FX + PI4 [ 52.709032][ C0] RIP: 0010:debug_print_object+0x196/0x290 [ 52.709509][ C0] Code: 48 89 fa 48 c1 ea 03 80 3c 02 00 0f 85e [ 52.711011][ C0] RSP: 0018:ffffc90000007cd0 EFLAGS: 00010282 [ 52.711510][ C0] RAX: 0000000000000000 RBX: 0000000000000003 0 [ 52.712125][ C0] RDX: ffffffff8c495800 RSI: ffffffff814b96d7 1 [ 52.712748][ C0] RBP: 0000000000000001 R08: 0000000000000001 0 [ 52.713370][ C0] R10: 0000000000000000 R11: 203a47554245444f 0 [ 52.713983][ C0] R13: ffffffff8aa6e960 R14: 0000000000000000 8 [ 52.714609][ C0] FS: 0000000000000000(0000) GS:ffff8881f5a000 [ 52.715356][ C0] CS: 0010 DS: 0000 ES: 0000 CR0: 000000008003 [ 52.715863][ C0] CR2: 000055914686f020 CR3: 000000011e856000 0 [ 52.716486][ C0] Call Trace: [ 52.716742][ C0] <IRQ> [ 52.716969][ C0] debug_check_no_obj_freed+0x302/0x420 [ 52.717423][ C0] slab_free_freelist_hook+0xec/0x1c0 [ 52.717848][ C0] ? rcu_core+0x818/0x1930 [ 52.718204][ C0] __kmem_cache_free+0xaf/0x2e0 [ 52.718590][ C0] rcu_core+0x818/0x1930 [ 52.718938][ C0] ? rcu_report_dead+0x610/0x610 [ 52.719328][ C0] __do_softirq+0x1d4/0x8ef [ 52.719689][ C0] __irq_exit_rcu+0x11d/0x190 [ 52.720062][ C0] irq_exit_rcu+0x9/0x20 [ 52.720402][ C0] sysvec_apic_timer_interrupt+0x97/0xc0 [ 52.720842][ C0] </IRQ> [ 52.721070][ C0] <TASK> [ 52.721300][ C0] asm_sysvec_apic_timer_interrupt+0x1a/0x20 [ 52.721779][ C0] RIP: 0010:default_idle+0xf/0x20 [ 52.722172][ C0] Code: 89 07 49 c7 c0 08 00 00 00 4d 29 c8 4c5 [ 52.723631][ C0] RSP: 0018:ffffffff8c407e30 EFLAGS: 00000202 [ 52.724096][ C0] RAX: 000000000007897f RBX: 0000000000000000 6 [ 52.724702][ C0] RDX: 0000000000000000 RSI: 0000000000000001 0 [ 52.725335][ C0] RBP: ffffffff8c495800 R08: 0000000000000001 b [ 52.725957][ C0] R10: ffffed103eb46d95 R11: 0000000000000000 0 [ 52.726550][ C0] R13: 0000000000000000 R14: ffffffff8e7834d0 0 [ 52.727162][ C0] ? ct_kernel_exit+0x1d6/0x240 [ 52.727542][ C0] default_idle_call+0x67/0xa0 [ 52.727912][ C0] do_idle+0x31e/0x3e0 [ 52.728241][ C0] ? arch_cpu_idle_exit+0x30/0x30 [ 52.728635][ C0] cpu_startup_entry+0x18/0x20 [ 52.729006][ C0] rest_init+0x16d/0x2b0 [ 52.729338][ C0] ? regulator_has_full_constraints+0x9/0x20 [ 52.729815][ C0] ? trace_init_perf_perm_irq_work_exit+0x20/00 [ 52.730309][ C0] arch_call_rest_init+0x13/0x30 [ 52.730703][ C0] start_kernel+0x352/0x4c0 [ 52.731087][ C0] secondary_startup_64_no_verify+0xce/0xdb [ 52.731611][ C0] </TASK> [ 52.731870][ C0] Kernel panic - not syncing: kernel: panic_on. [ 52.732445][ C0] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 6.0 [ 52.733140][ C0] Hardware name: QEMU Standard PC (i440FX + PI4 [ 52.733867][ C0] Call Trace: [ 52.734143][ C0] <IRQ> [ 52.734380][ C0] dump_stack_lvl+0xd9/0x150 [ 52.734769][ C0] panic+0x684/0x730 [ 52.735082][ C0] ? panic_smp_self_stop+0x90/0x90 [ 52.735322][ C0] ? show_trace_log_lvl+0x285/0x390 [ 52.735322][ C0] ? debug_print_object+0x196/0x290 [ 52.735322][ C0] check_panic_on_warn+0xb1/0xc0 [ 52.735322][ C0] __warn+0xf2/0x390 [ 52.735322][ C0] ? debug_print_object+0x196/0x290 [ 52.735322][ C0] report_bug+0x2dd/0x500 [ 52.735322][ C0] handle_bug+0x3c/0x70 [ 52.735322][ C0] exc_invalid_op+0x18/0x50 [ 52.735322][ C0] asm_exc_invalid_op+0x1a/0x20 [ 52.735322][ C0] RIP: 0010:debug_print_object+0x196/0x290 [ 52.735322][ C0] Code: 48 89 fa 48 c1 ea 03 80 3c 02 00 0f 85e [ 52.735322][ C0] RSP: 0018:ffffc90000007cd0 EFLAGS: 00010282 [ 52.735322][ C0] RAX: 0000000000000000 RBX: 0000000000000003 0 [ 52.735322][ C0] RDX: ffffffff8c495800 RSI: ffffffff814b96d7 1 [ 52.735322][ C0] RBP: 0000000000000001 R08: 0000000000000001 0 [ 52.735322][ C0] R10: 0000000000000000 R11: 203a47554245444f 0 [ 52.735322][ C0] R13: ffffffff8aa6e960 R14: 0000000000000000 8 [ 52.735322][ C0] ? __warn_printk+0x187/0x310 [ 52.735322][ C0] debug_check_no_obj_freed+0x302/0x420 [ 52.735322][ C0] slab_free_freelist_hook+0xec/0x1c0 [ 52.735322][ C0] ? rcu_core+0x818/0x1930 [ 52.735322][ C0] __kmem_cache_free+0xaf/0x2e0 [ 52.735322][ C0] rcu_core+0x818/0x1930 [ 52.735322][ C0] ? rcu_report_dead+0x610/0x610 [ 52.735322][ C0] __do_softirq+0x1d4/0x8ef [ 52.735322][ C0] __irq_exit_rcu+0x11d/0x190 [ 52.735322][ C0] irq_exit_rcu+0x9/0x20 [ 52.735322][ C0] sysvec_apic_timer_interrupt+0x97/0xc0 [ 52.735322][ C0] </IRQ> [ 52.735322][ C0] <TASK> [ 52.735322][ C0] asm_sysvec_apic_timer_interrupt+0x1a/0x20 [ 52.735322][ C0] RIP: 0010:default_idle+0xf/0x20 [ 52.735322][ C0] Code: 89 07 49 c7 c0 08 00 00 00 4d 29 c8 4c5 [ 52.735322][ C0] RSP: 0018:ffffffff8c407e30 EFLAGS: 00000202 [ 52.735322][ C0] RAX: 000000000007897f RBX: 0000000000000000 6 [ 52.735322][ C0] RDX: 0000000000000000 RSI: 0000000000000001 0 [ 52.735322][ C0] RBP: ffffffff8c495800 R08: 0000000000000001 b [ 52.735322][ C0] R10: ffffed103eb46d95 R11: 0000000000000000 0 [ 52.735322][ C0] R13: 0000000000000000 R14: ffffffff8e7834d0 0 [ 52.735322][ C0] ? ct_kernel_exit+0x1d6/0x240 [ 52.735322][ C0] default_idle_call+0x67/0xa0 [ 52.735322][ C0] do_idle+0x31e/0x3e0 [ 52.735322][ C0] ? arch_cpu_idle_exit+0x30/0x30 [ 52.735322][ C0] cpu_startup_entry+0x18/0x20 [ 52.735322][ C0] rest_init+0x16d/0x2b0 [ 52.735322][ C0] ? regulator_has_full_constraints+0x9/0x20 [ 52.735322][ C0] ? trace_init_perf_perm_irq_work_exit+0x20/00 [ 52.735322][ C0] arch_call_rest_init+0x13/0x30 [ 52.735322][ C0] start_kernel+0x352/0x4c0 [ 52.735322][ C0] secondary_startup_64_no_verify+0xce/0xdb [ 52.735322][ C0] </TASK> [ 52.735322][ C0] Kernel Offset: disabled [ 52.735322][ C0] Rebooting in 86400 seconds..
Hi all,
Quoted 7 lines · Jamal Hadi Salim
To provide more update:
Happens on single processor before Seth's patches; and only on multi-processor after Seth's patches.
Theory is: there is a logic bug in the miniqdisc rcu visibility. Feels like the freeing of the structure is done without rcu involvement. Jiri/Cong maybe you can take a look since youve been dabbling in miniqdisc? The reproducer worked for me and Pedro 100% of the time.
I also reproduced this UAF using the syzkaller reproducer in the report (the C reproducer did not work for me for unknown reasons). I will look into this.
Thanks,
Peilin Ye
+Cc: Vlad Buslov, Hillf Danton
Hi all,
Quoted 3 lines · Peilin Ye
I also reproduced this UAF using the syzkaller reproducer in the report (the C reproducer did not work for me for unknown reasons). I will look into this.
Currently, multiple ingress (clsact) Qdiscs can access the per-netdev *miniq_ingress (*miniq_egress) pointer concurrently. This is unfortunately true in two senses:
1. We allow adding ingress (clsact) Qdiscs under parents other than TC_H_INGRESS (TC_H_CLSACT):
$ ip link add ifb0 numtxqueues 8 type ifb $ echo clsact > /proc/sys/net/core/default_qdisc $ tc qdisc add dev ifb0 handle 1: root mq $ tc qdisc show dev ifb0 qdisc mq 1: root qdisc clsact 0: parent 1:8 qdisc clsact 0: parent 1:7 qdisc clsact 0: parent 1:6 qdisc clsact 0: parent 1:5 qdisc clsact 0: parent 1:4 qdisc clsact 0: parent 1:3 qdisc clsact 0: parent 1:2 qdisc clsact 0: parent 1:1
This is obviously racy and should be prohibited. I've started working on patches to fix this. The syz repro for this UAF adds ingress Qdiscs under TC_H_ROOT, by the way.
2. After introducing RTNL-lockless RTM_{NEW,DEL,GET}TFILTER requests [1], it is possible that, when replacing ingress (clsact) Qdiscs, the old one can access *miniq_{in,e}gress concurrently with the new one. For example, the syz repro does something like the following:
Thread 1 creates sch_ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a cls_flower filter X to Qdisc A.
Thread 2 creates sch_ingress Qdisc B (containing mini Qdisc b1 and b2) to replace Qdisc A, then adds a cls_flower filter Y to Qdisc B.
Device has 8 TXQs.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC (A, locked)
qdisc_create(A) 1
qdisc_graft(A) 9
RTM_NEWTFILTER (X, lockless)
__tcf_qdisc_find(A) 10
tcf_chain0_head_change(A)
! mini_qdisc_pair_swap(A)
| RTM_NEWQDISC (B, locked)
| 2 qdisc_graft(B)
| 1 notify_and_destroy(A)
|
| RTM_NEWTFILTER (Y, lockless)
| tcf_chain0_head_change(B)
| ! mini_qdisc_pair_swap(B)
tcf_block_release(A) 0 |
qdisc_destroy(A) |
tcf_chain0_head_change_cb_del(A) |
! mini_qdisc_pair_swap(A) |
| |... ...
As we can see there're interleaving mini_qdisc_pair_swap() calls between Qdisc A and B, causing all kinds of troubles, including the UAF (thread 2 writing to mini Qdisc a1's rcu_state after Qdisc A has already been freed) reported by syzbot.
To fix this, I'm cooking a patch that, when replacing ingress (clsact) Qdiscs, in qdisc_graft():
I. We should make sure there's no on-the-fly lockless filter requests
for the old Qdisc, and return -EBUSY if there's any (or can/should we wait in RTM_NEWQDISC handler?)
II. We should destory the old Qdisc before publishing the new one
(i.e. setting it to dev_ingress_queue(dev)->qdisc_sleeping, so that subsequent filter requests can see it), because {ingress,clsact}_destroy() also call mini_qdisc_pair_swap(), which sets *miniq_{in,e}gress to NULL
Future Qdiscs that support RTNL-lockless cls_ops, if any, won't need this fix, as long as their ->chain_head_change() don't access out-of-Qdisc-scope data, like pointers in struct net_device.
Do you think this is the right way to go? Thanks!
[1] Thanks Hillf Danton for the hint:
https://syzkaller.appspot.com/text?tag=Patch&x=10d7cd5bc80000
Thanks,
Peilin Ye
Quoted 68 lines · Peilin Ye
+Cc: Vlad Buslov, Hillf Danton
Hi all,
Quoted 3 lines · Peilin Ye
I also reproduced this UAF using the syzkaller reproducer in the report (the C reproducer did not work for me for unknown reasons). I will look into this.
Currently, multiple ingress (clsact) Qdiscs can access the per-netdev
*miniq_ingress(*miniq_egress) pointer concurrently. This is unfortunately true in two senses:1. We allow adding ingress (clsact) Qdiscs under parents other than
TC_H_INGRESS(TC_H_CLSACT):$ ip link add ifb0 numtxqueues 8 type ifb $ echo clsact > /proc/sys/net/core/default_qdisc $ tc qdisc add dev ifb0 handle 1: root mq $ tc qdisc show dev ifb0 qdisc mq 1: root qdisc clsact 0: parent 1:8 qdisc clsact 0: parent 1:7 qdisc clsact 0: parent 1:6 qdisc clsact 0: parent 1:5 qdisc clsact 0: parent 1:4 qdisc clsact 0: parent 1:3 qdisc clsact 0: parent 1:2 qdisc clsact 0: parent 1:1This is obviously racy and should be prohibited. I've started working on patches to fix this. The syz repro for this UAF adds ingress Qdiscs under
TC_H_ROOT, by the way.2. After introducing RTNL-lockless RTM_{NEW,DEL,GET}TFILTER requests [1], it is possible that, when replacing ingress (clsact) Qdiscs, the old one can access *miniq_{in,e}gress concurrently with the new one. For example, the syz repro does something like the following:
Thread 1 creates
sch_ingressQdisc A (containing mini Qdisc a1 and a2), then adds acls_flowerfilter X to Qdisc A.Thread 2 creates
sch_ingressQdisc B (containing mini Qdisc b1 and b2) to replace Qdisc A, then adds acls_flowerfilter Y to Qdisc B.Device has 8 TXQs.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, lockless)__tcf_qdisc_find(A) 10 tcf_chain0_head_change(A) ! mini_qdisc_pair_swap(A) | RTM_NEWQDISC (B, locked) | 2 qdisc_graft(B) | 1 notify_and_destroy(A) | | RTM_NEWTFILTER (Y, lockless) | tcf_chain0_head_change(B) | ! mini_qdisc_pair_swap(B) tcf_block_release(A) 0 | qdisc_destroy(A) | tcf_chain0_head_change_cb_del(A) | ! mini_qdisc_pair_swap(A) | | |
Quoted 6 lines
... ...
As we can see there're interleaving
mini_qdisc_pair_swap()calls between Qdisc A and B, causing all kinds of troubles, including the UAF (thread 2 writing to mini Qdisc a1'srcu_stateafter Qdisc A has already been freed) reported by syzbot.
Thanks for the analysis. It makes total sense.
After going through the call chains, please correct me if my ELI5 is wrong:
'clsact_init()' is called for B when dev has miniq_ingress set to 'A', therefore copying a pointer to the miniq_qdisc with lifetime bound to 'A' in a miniq_qdisc_pair with lifetime bound to 'B' therefore raising an UAF after A is destroyed and B is manipulated.
Quoted 7 lines
To fix this, I'm cooking a patch that, when replacing ingress (clsact) Qdiscs, in
qdisc_graft():I. We should make sure there's no on-the-fly lockless filter requests
for the old Qdisc, and return-EBUSYif there's any (or can/should we wait inRTM_NEWQDISChandler?
Makes sense.
Quoted 9 lines
II. We should destory the old Qdisc before publishing the new one
(i.e. setting it to dev_ingress_queue(dev)->qdisc_sleeping, so that subsequent filter requests can see it), because {ingress,clsact}_destroy() also callmini_qdisc_pair_swap(), which sets *miniq_{in,e}gress to NULL >
Future Qdiscs that support RTNL-locklesscls_ops, if any, won't need this fix, as long as their ->chain_head_change() don't access out-of-Qdisc-scope data, like pointers in structnet_device.
Probably worth a comment somewhere in the code
Quoted 9 lines
Do you think this is the right way to go? Thanks!
[1] Thanks Hillf Danton for the hint:
https://syzkaller.appspot.com/text?tag=Patch&x=10d7cd5bc80000Thanks,
Peilin Ye
Hi Peilin,
Quoted 30 lines · Peilin Ye
+Cc: Vlad Buslov, Hillf Danton
Hi all,
Quoted 3 lines · Peilin Ye
I also reproduced this UAF using the syzkaller reproducer in the report (the C reproducer did not work for me for unknown reasons). I will look into this.
Currently, multiple ingress (clsact) Qdiscs can access the per-netdev
*miniq_ingress(*miniq_egress) pointer concurrently. This is unfortunately true in two senses:1. We allow adding ingress (clsact) Qdiscs under parents other than
TC_H_INGRESS(TC_H_CLSACT):$ ip link add ifb0 numtxqueues 8 type ifb $ echo clsact > /proc/sys/net/core/default_qdisc $ tc qdisc add dev ifb0 handle 1: root mq $ tc qdisc show dev ifb0 qdisc mq 1: root qdisc clsact 0: parent 1:8 qdisc clsact 0: parent 1:7 qdisc clsact 0: parent 1:6 qdisc clsact 0: parent 1:5 qdisc clsact 0: parent 1:4 qdisc clsact 0: parent 1:3 qdisc clsact 0: parent 1:2 qdisc clsact 0: parent 1:1
Quoted 3 lines
This is obviously racy and should be prohibited. I've started working on patches to fix this. The syz repro for this UAF adds ingress Qdiscs under
TC_H_ROOT, by the way.
Hmm, didn't realize it was the case.
Quoted 35 lines
2. After introducing RTNL-lockless RTM_{NEW,DEL,GET}TFILTER requests [1], it is possible that, when replacing ingress (clsact) Qdiscs, the old one can access *miniq_{in,e}gress concurrently with the new one. For example, the syz repro does something like the following:
Thread 1 creates
sch_ingressQdisc A (containing mini Qdisc a1 and a2), then adds acls_flowerfilter X to Qdisc A.Thread 2 creates
sch_ingressQdisc B (containing mini Qdisc b1 and b2) to replace Qdisc A, then adds acls_flowerfilter Y to Qdisc B.Device has 8 TXQs.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
!mini_qdisc_pair_swap(A)| RTM_NEWQDISC (B, locked) | 2 qdisc_graft(B) | 1 notify_and_destroy(A) | | RTM_NEWTFILTER (Y, lockless) | tcf_chain0_head_change(B) | ! mini_qdisc_pair_swap(B) tcf_block_release(A) 0 | qdisc_destroy(A) | tcf_chain0_head_change_cb_del(A) | ! mini_qdisc_pair_swap(A) | | |
Quoted 6 lines
... ...
As we can see there're interleaving
mini_qdisc_pair_swap()calls between Qdisc A and B, causing all kinds of troubles, including the UAF (thread 2 writing to mini Qdisc a1'srcu_stateafter Qdisc A has already been freed) reported by syzbot.
Great analysis! However, it is still not quite clear to me how threads 1 and 2 access each other RCU state when q->miniqp is a private memory of the Qdisc, so 1 should only see A->miniqp and 2 only B->miniqp. And both miniqps should be protected from deallocation by reference that lockless RTM_NEWTFILTER obtains.
Quoted 8 lines
To fix this, I'm cooking a patch that, when replacing ingress (clsact) Qdiscs, in
qdisc_graft():I. We should make sure there's no on-the-fly lockless filter requests
for the old Qdisc, and return-EBUSYif there's any (or can/should we wait inRTM_NEWQDISChandler?)
Quoted 5 lines
II. We should destory the old Qdisc before publishing the new one
(i.e. setting it to dev_ingress_queue(dev)->qdisc_sleeping, so that subsequent filter requests can see it), because {ingress,clsact}_destroy() also callmini_qdisc_pair_swap(), which sets *miniq_{in,e}gress to NULL
Another approach would be to somehow detect concurrent Qdisc replace and return -EAGAIN from tcf_chain_tp_insert() before calling tcf_chain0_head_change(). This would leverage existing cls_api functionality that automatically retries after releasing all references to chain/tp and obtaining them again instead of messing with qdisc api. However, since I still didn't fully grasp the issue it is hard for me to reason whether such approach would be possible to implement in this case.
Quoted 12 lines
Future Qdiscs that support RTNL-lockless
cls_ops, if any, won't need this fix, as long as their ->chain_head_change() don't access out-of-Qdisc-scope data, like pointers in structnet_device.Do you think this is the right way to go? Thanks!
[1] Thanks Hillf Danton for the hint:
https://syzkaller.appspot.com/text?tag=Patch&x=10d7cd5bc80000Thanks,
Peilin Ye
Hi Pedro, Vlad,
Quoted 11 lines · Vlad Buslov
Quoted 4 lines · Peilin Ye
As we can see there're interleaving
mini_qdisc_pair_swap()calls between Qdisc A and B, causing all kinds of troubles, including the UAF (thread 2 writing to mini Qdisc a1'srcu_stateafter Qdisc A has already been freed) reported by syzbot.Great analysis! However, it is still not quite clear to me how threads 1 and 2 access each other RCU state when
q->miniqpis a private memory of the Qdisc, so 1 should only seeA->miniqpand 2 onlyB->miniqp. And both miniqps should be protected from deallocation by reference that locklessRTM_NEWTFILTERobtains.
Thanks for taking a look!
To elaborate, p_miniq is a pointer of pointer of struct mini_Qdisc, initialized in ingress_init() to point to eth0->miniq_ingress, which isn't private to A or B.
In other words, both A->miniqp->p_miniq and B->miniqp->p_miniq point to eth0->miniq_ingress.
For your reference, roughly speaking, mini_qdisc_pair_swap() does this:
miniq_old = dev->miniq_ingress;
if (destroying) {
dev->miniq_ingress = NULL;
} else {
rcu_wait();
dev->miniq_ingress = miniq_new;
} if (miniq_old)
miniq_old->rcu_state = ...
Quoted 9 lines · Peilin Ye
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
!mini_qdisc_pair_swap(A)
1. A adds its first filter,
miniq_old (eth0->miniq_ingress) is NULL, RCU wait starts,
RCU wait ends,
change eth0->miniq_ingress to A's mini Qdisc.
Quoted 7 lines
| RTM_NEWQDISC (B, locked)
| 2 qdisc_graft(B)
| 1 notify_and_destroy(A)
|
| RTM_NEWTFILTER (Y, lockless)
| tcf_chain0_head_change(B)
| ! mini_qdisc_pair_swap(B) 2. B adds its first filter,
miniq_old (eth0->miniq_ingress) is A's mini Qdisc, RCU wait starts,
Quoted 4 lines
tcf_block_release(A) 0 | qdisc_destroy(A) | tcf_chain0_head_change_cb_del(A) | ! mini_qdisc_pair_swap(A) |
3. A destroys itself,
miniq_old (eth0->miniq_ingress) is A's mini Qdisc, (destroying, so no RCU wait)
change eth0->miniq_ingress to NULL, update miniq_old, or A's mini Qdisc's RCU state, A is freed.
2. RCU wait ends,
change eth0->miniq_ingress to B's mini Qdisc,
use-after-free: update miniq_old, or A's mini Qdisc's RCU state.
I hope this helps. Sorry I didn't go into details; this UAF isn't the only thing that is unacceptable here:
Consider B. We add a filter Y to B, expecting ingress packets on eth0 to go through Y. Then all of a sudden, A sets eth0->miniq_ingress to NULL during its destruction, so packets will not find Y at all on datapath (sch_handle_ingress()). New filter becomes invisible - this is already buggy enough :-/
So I think B's first call to mini_qdisc_pair_swap() should happen after A's last call (in ingress_destroy()), which is what I am trying to achieve here.
Thanks,
Peilin Ye
Quoted 79 lines · Peilin Ye
Hi Pedro, Vlad,
Quoted 11 lines · Vlad Buslov
Quoted 4 lines · Peilin Ye
As we can see there're interleaving
mini_qdisc_pair_swap()calls between Qdisc A and B, causing all kinds of troubles, including the UAF (thread 2 writing to mini Qdisc a1'srcu_stateafter Qdisc A has already been freed) reported by syzbot.Great analysis! However, it is still not quite clear to me how threads 1 and 2 access each other RCU state when
q->miniqpis a private memory of the Qdisc, so 1 should only seeA->miniqpand 2 onlyB->miniqp. And both miniqps should be protected from deallocation by reference that locklessRTM_NEWTFILTERobtains.Thanks for taking a look!
To elaborate,
p_miniqis a pointer of pointer of structmini_Qdisc, initialized iningress_init()to point toeth0->miniq_ingress, which isn't private to A or B.In other words, both
A->miniqp->p_miniqandB->miniqp->p_miniqpoint toeth0->miniq_ingress.For your reference, roughly speaking,
mini_qdisc_pair_swap()does this:miniq_old = dev->miniq_ingress; if (destroying) { dev->miniq_ingress = NULL; } else { rcu_wait(); dev->miniq_ingress = miniq_new; }if (
miniq_old)miniq_old->rcu_state = ...Quoted 9 lines · Peilin Ye
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
!mini_qdisc_pair_swap(A)1. A adds its first filter,
miniq_old(eth0->miniq_ingress) is NULL, RCU wait starts,
RCU wait ends,
changeeth0->miniq_ingressto A's mini Qdisc.Quoted 7 lines
| RTM_NEWQDISC (B, locked) | 2 qdisc_graft(B) | 1 notify_and_destroy(A) | | RTM_NEWTFILTER (Y, lockless) | tcf_chain0_head_change(B) | ! mini_qdisc_pair_swap(B)2. B adds its first filter,
miniq_old(eth0->miniq_ingress) is A's mini Qdisc, RCU wait starts,Quoted 4 lines
tcf_block_release(A) 0 | qdisc_destroy(A) | tcf_chain0_head_change_cb_del(A) | ! mini_qdisc_pair_swap(A) |3. A destroys itself,
miniq_old(eth0->miniq_ingress) is A's mini Qdisc, (destroying, so no RCU wait)
changeeth0->miniq_ingressto NULL, updateminiq_old, or A's mini Qdisc's RCU state, A is freed.
Quoted 3 lines
2. RCU wait ends,
changeeth0->miniq_ingressto B's mini Qdisc,
use-after-free: updateminiq_old, or A's mini Qdisc's RCU state.
Thanks for the clarification.
Quoted 10 lines
I hope this helps. Sorry I didn't go into details; this UAF isn't the only thing that is unacceptable here:
Consider B. We add a filter Y to B, expecting ingress packets on eth0 to go through Y. Then all of a sudden, A sets
eth0->miniq_ingressto NULL during its destruction, so packets will not find Y at all on datapath (sch_handle_ingress()). New filter becomes invisible - this is already buggy enough :-/
Quoted 3 lines
So I think B's first call to
mini_qdisc_pair_swap()should happen after A's last call (iningress_destroy()), which is what I am trying to achieve here.
Makes sense to me.
cover net/sched: Fixes for sch_ingress and sch_clsact
In reply to Peilin YeHi all,
These are fixes for ingress and clsact Qdiscs:
[1,2/6]: ingress and clsact Qdiscs should only be created under ffff:fff1
[3/6]: Under ffff:fff1, only create ingress and clsact Qdiscs (for now,
at least)
[4/6]: After creating ingress and clsact Qdiscs under ffff:fff1, do not
graft them again to anywhere else (e.g. as the inner Qdisc of a TBF Qdisc)
[5/6]: Prepare for [6/6], do not reuse that for-loop in qdisc_graft()
for ingress and clsact Qdiscs
[6/6]: Fix use-after-free [a] in mini_qdisc_pair_swap()
Please review (especially [6/6]), thanks! Other tasks, including:
- prohibiting sch_ingress, sch_clsact etc. in default_qdisc
- more cleanups for qdisc_graft()
Will be done in separate patches.
[a] https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b
Thanks,
Peilin Ye (6):
net/sched: sch_ingress: Only create under TC_H_INGRESS net/sched: sch_clsact: Only create under TC_H_CLSACT net/sched: Reserve TC_H_INGRESS (TC_H_CLSACT) for ingress (clsact)
Qdiscs
net/sched: Prohibit regrafting ingress or clsact Qdiscs net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs net/sched: qdisc_destroy() old ingress and clsact Qdiscs before
grafting
include/net/sch_generic.h | 8 ++++++ net/sched/sch_api.c | 54 +++++++++++++++++++++++++++++---------- net/sched/sch_generic.c | 14 +++++++--- net/sched/sch_ingress.c | 10 ++++++-- 4 files changed, 67 insertions(+), 19 deletions(-)
Signature · 1 line
2.20.1
PATCH 1/6 net/sched: sch_ingress: Only create under TC_H_INGRESS
ingress Qdiscs are only supposed to be created under TC_H_INGRESS. Similar to mq_init(), return -EOPNOTSUPP if 'parent' is not TC_H_INGRESS.
--- net/sched/sch_ingress.c | 3 +++ 1 file changed, 3 insertions(+)
net/sched/sch_ingress.c +3 -0
@@ -80,6 +80,9 @@ static int ingress_init(struct Qdisc *sch, struct nlattr *opt,struct net_device *dev =qdisc_dev (sch);int err; +if (sch->parent !=TC_H_INGRESS ) +return -EOPNOTSUPP ; +net_inc_ingress_queue ();mini_qdisc_pair_init (&q->miniqp, sch, &dev->miniq_ingress);
Signature · 1 line
2.20.1
PATCH 2/6 net/sched: sch_clsact: Only create under TC_H_CLSACT
In reply to Peilin Yeclsact Qdiscs are only supposed to be created under TC_H_CLSACT (which equals TC_H_INGRESS). Return -EOPNOTSUPP if 'parent' is not TC_H_CLSACT.
--- net/sched/sch_ingress.c | 3 +++ 1 file changed, 3 insertions(+)
net/sched/sch_ingress.c +3 -0
@@ -222,6 +222,9 @@ static int clsact_init(struct Qdisc *sch, struct nlattr *opt,struct net_device *dev =qdisc_dev (sch);int err; +if (sch->parent !=TC_H_CLSACT ) +return -EOPNOTSUPP ; +net_inc_ingress_queue ();net_inc_egress_queue ();
Signature · 1 line
2.20.1
PATCH 3/6 net/sched: Reserve TC_H_INGRESS (TC_H_CLSACT) for ingress (clsact) Qdiscs
In reply to Peilin YeCurrently it is possible to add e.g. an HTB Qdisc under ffff:fff1 (TC_H_INGRESS, TC_H_CLSACT):
$ ip link add name ifb0 type ifb $ tc qdisc add dev ifb0 parent ffff:fff1 htb $ tc qdisc add dev ifb0 clsact Error: Exclusivity flag on, cannot modify. $ drgn ...
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
ifb0 =
netdev_get_by_name(prog, "ifb0")
qdisc = ifb0.ingress_queue.qdisc_sleeping
print(qdisc.ops.id.string_().decode())
htb
Quoted 1 line
Quoted 1 line
Quoted 1 line
qdisc.flags.value_()#TCQ_F_INGRESS
2
Only allow ingress and clsact Qdiscs under ffff:fff1. Return -EINVAL for everything else. Make TCQ_F_INGRESS a static flag of ingress and clsact Qdiscs.
--- net/sched/sch_api.c | 7 ++++++- net/sched/sch_ingress.c | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-)
net/sched/sch_api.c +6 -1
@@ -1252,7 +1252,12 @@ static struct Qdisc *qdisc_create(struct net_device *dev, sch->parent = parent;if (handle ==TC_H_INGRESS ) { - sch->flags |=TCQ_F_INGRESS ; +if (!(sch->flags &TCQ_F_INGRESS )) { +NL_SET_ERR_MSG (extack, +"Specified parent ID is reserved for ingress and clsact Qdiscs" ); + err = -EINVAL ; + goto err_out3; + } handle =TC_H_MAKE (TC_H_INGRESS ,0 ); }else {if (handle ==0 ) {
net/sched/sch_ingress.c +2 -2
@@ -137,7 +137,7 @@ static struct Qdisc_ops ingress_qdisc_ops __read_mostly = { .cl_ops = &ingress_class_ops, .id = "ingress", .priv_size = sizeof(struct ingress_sched_data), - .static_flags = TCQ_F_CPUSTATS, + .static_flags = TCQ_F_INGRESS | TCQ_F_CPUSTATS, .init = ingress_init, .destroy = ingress_destroy, .dump = ingress_dump,
@@ -275,7 +275,7 @@ static struct Qdisc_ops clsact_qdisc_ops __read_mostly = { .cl_ops = &clsact_class_ops, .id = "clsact", .priv_size = sizeof(struct clsact_sched_data), - .static_flags = TCQ_F_CPUSTATS, + .static_flags = TCQ_F_INGRESS | TCQ_F_CPUSTATS, .init = clsact_init, .destroy = clsact_destroy, .dump = ingress_dump,
Signature · 1 line
2.20.1
PATCH 4/6 net/sched: Prohibit regrafting ingress or clsact Qdiscs
In reply to Peilin YeCurrently, after creating an ingress (or clsact) Qdisc and grafting it under TC_H_INGRESS (TC_H_CLSACT), it is possible to graft it again under e.g. a TBF Qdisc:
$ ip link add ifb0 type ifb
$ tc qdisc add dev ifb0 handle 1: root tbf rate 20kbit buffer 1600 limit 3000
$ tc qdisc add dev ifb0 clsact
$ tc qdisc link dev ifb0 handle ffff: parent 1:1
$ tc qdisc show dev ifb0
qdisc tbf 1: root refcnt 2 rate 20Kbit burst 1600b lat 560.0ms
qdisc clsact ffff: parent ffff:fff1 refcnt 2
^^^^^^^^clsact's refcount has increased: it is now grafted under both TC_H_CLSACT and 1:1.
ingress and clsact Qdiscs should only be used under TC_H_INGRESS (TC_H_CLSACT). Prohibit regrafting them.
--- net/sched/sch_api.c | 5 +++++ 1 file changed, 5 insertions(+)
net/sched/sch_api.c +5 -0
@@ -1596,6 +1596,11 @@ static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n,NL_SET_ERR_MSG (extack,"Invalid qdisc name" );return -EINVAL ; } +if (q->flags &TCQ_F_INGRESS ) { +NL_SET_ERR_MSG (extack, +"Cannot regraft ingress or clsact Qdiscs" ); +return -EINVAL ; + }if (q == p || (p &&check_loop (q, p,0 ))) {NL_SET_ERR_MSG (extack,"Qdisc parent/child loop detected" );
Signature · 1 line
2.20.1
PATCH 5/6 net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs
In reply to Peilin YeGrafting ingress and clsact Qdiscs does not need a for-loop in qdisc_graft(). Refactor it. No functional changes intended.
--- net/sched/sch_api.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-)
net/sched/sch_api.c +10 -10
@@ -1073,12 +1073,12 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (parent ==NULL ) {unsigned int i, num_q, ingress; +struct netdev_queue *dev_queue; ingress =0 ; num_q = dev->num_tx_queues;if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { - num_q =1 ; ingress =1 ;if (!dev_ingress_queue (dev)) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );
@@ -1094,18 +1094,18 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach && !ingress) goto skip; -for (i =0 ; i < num_q; i++) { -struct netdev_queue *dev_queue =dev_ingress_queue (dev); - -if (!ingress) +if (!ingress) { +for (i =0 ; i < num_q; i++) { dev_queue =netdev_get_tx_queue (dev, i); + old =dev_graft_qdisc (dev_queue, new); - old =dev_graft_qdisc (dev_queue, new); -if (new && i >0 ) -qdisc_refcount_inc (new); - -if (!ingress) +if (new && i >0 ) +qdisc_refcount_inc (new);qdisc_put (old); + } + }else { + dev_queue =dev_ingress_queue (dev); + old =dev_graft_qdisc (dev_queue, new); } skip:
Signature · 1 line
2.20.1
PATCH 6/6 net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Peilin Yemini_Qdisc_pair::p_miniq is a double pointer to mini_Qdisc, initialized in ingress_init() to point to net_device::miniq_ingress. ingress Qdiscs access this per-net_device pointer in mini_qdisc_pair_swap(). Similar for clsact Qdiscs and miniq_egress.
Unfortunately, after introducing RTNL-lockless RTM_{NEW,DEL,GET}TFILTER requests, when e.g. replacing ingress (clsact) Qdiscs, the old Qdisc could access the same miniq_{in,e}gress pointer(s) concurrently with the new Qdisc, causing race conditions [1] including a use-after-free in mini_qdisc_pair_swap() reported by syzbot:
BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 ... Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266
...
The new (ingress or clsact) Qdisc should only call mini_qdisc_pair_swap() after the old Qdisc's last call (in {ingress,clsact}_destroy()) has finished.
To achieve this, in qdisc_graft(), return -EBUSY if the old (ingress or clsact) Qdisc has ongoing RTNL-lockless filter requests, and call qdisc_destroy() for "old" before grafting "new".
Introduce qdisc_refcount_dec_if_one() as the counterpart of qdisc_refcount_inc_nz() used for RTNL-lockless filter requests. Introduce a non-static version of qdisc_destroy() that does a TCQ_F_BUILTIN check, just like qdisc_put() etc.
[1] To illustrate, the syzkaller reproducer adds ingress Qdiscs under TC_H_ROOT (no longer possible after patch "net/sched: sch_ingress: Only create under TC_H_INGRESS") on eth0 that has 8 transmission queues:
Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC (A, RTNL-locked)
qdisc_create(A) 1
qdisc_graft(A) 9
RTM_NEWTFILTER (X, RTNL-lockless)
__tcf_qdisc_find(A) 10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A) (1st)
|
| RTM_NEWQDISC (B, RTNL-locked)
RCU 2 qdisc_graft(B)
| 1 notify_and_destroy(A)
|
tcf_block_release(A) 0 RTM_NEWTFILTER (Y, RTNL-lockless)
qdisc_destroy(A) tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A) mini_qdisc_pair_swap(B) (2nd)
mini_qdisc_pair_swap(A) (3rd) |
... ...
Here, B calls mini_qdisc_pair_swap(), pointing eth0->miniq_ingress to its mini Qdisc, b1. Then, A calls mini_qdisc_pair_swap() again during ingress_destroy(), setting eth0->miniq_ingress to NULL, so ingress packets on eth0 will not find filter Y in sch_handle_ingress().
This is just one of the possible consequences of concurrently accessing net_device::miniq_{in,e}gress pointers. The point is clear, however:
B's first call to mini_qdisc_pair_swap() should take place after A's last call, in qdisc_destroy().
--- include/net/sch_generic.h | 8 ++++++++ net/sched/sch_api.c | 26 +++++++++++++++++++++----- net/sched/sch_generic.c | 14 +++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-)
include/net/sch_generic.h +8 -0
@@ -137,6 +137,13 @@ static inline void qdisc_refcount_inc(struct Qdisc *qdisc)refcount_inc (&qdisc->refcnt); } +static inline bool qdisc_refcount_dec_if_one (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return true; +return refcount_dec_if_one (&qdisc->refcnt); +} +/* Intended to be used by unlocked users, when concurrent qdisc release is * possible. */
@@ -652,6 +659,7 @@ void dev_deactivate_many(struct list_head *head);struct Qdisc *dev_graft_qdisc (struct netdev_queue *dev_queue,struct Qdisc *qdisc);void qdisc_reset (struct Qdisc *qdisc); +void qdisc_destroy (struct Qdisc *qdisc);void qdisc_put (struct Qdisc *qdisc);void qdisc_put_unlocked (struct Qdisc *qdisc);void qdisc_tree_reduce_backlog (struct Qdisc *qdisc,int n,int len);
net/sched/sch_api.c +21 -5
@@ -1080,10 +1080,20 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { ingress =1 ; -if (!dev_ingress_queue (dev)) { + dev_queue =dev_ingress_queue (dev); +if (!dev_queue) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );return -ENOENT ; } + +/* This is the counterpart of that qdisc_refcount_inc_nz() call in +* __tcf_qdisc_find() for RTNL-lockless filter requests. +*/ +if (!qdisc_refcount_dec_if_one (dev_queue->qdisc_sleeping)) { +NL_SET_ERR_MSG (extack, +"Current ingress or clsact Qdisc has ongoing filter request(s)" ); +return -EBUSY ; + } }if (dev->flags &IFF_UP )
@@ -1104,8 +1114,16 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,qdisc_put (old); } }else { - dev_queue =dev_ingress_queue (dev); - old =dev_graft_qdisc (dev_queue, new); + old =dev_graft_qdisc (dev_queue,NULL ); + +/* {ingress,clsact}_destroy() "old" before grafting "new" to avoid +* unprotected concurrent accesses to net_device::miniq_{in,e}gress +* pointer(s) in mini_qdisc_pair_swap(). +*/ +qdisc_notify (net, skb, n, classid, old, new, extack); +qdisc_destroy (old); + +dev_graft_qdisc (dev_queue, new); } skip:
@@ -1119,8 +1137,6 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach) new->ops->attach (new); - }else { -notify_and_destroy (net, skb, n, classid, old, new, extack); }if (dev->flags &IFF_UP )
net/sched/sch_generic.c +11 -3
@@ -1046,7 +1046,7 @@ static void qdisc_free_cb(struct rcu_head *head)qdisc_free (q); } -static void qdisc_destroy (struct Qdisc *qdisc) +static void __qdisc_destroy (struct Qdisc *qdisc) {const struct Qdisc_ops *ops = qdisc->ops;
@@ -1070,6 +1070,14 @@ static void qdisc_destroy(struct Qdisc *qdisc)call_rcu (&qdisc->rcu, qdisc_free_cb); } +void qdisc_destroy (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return ; + +__qdisc_destroy (qdisc); +} +void qdisc_put (struct Qdisc *qdisc) {if (!qdisc)
@@ -1079,7 +1087,7 @@ void qdisc_put(struct Qdisc *qdisc) !refcount_dec_and_test(&qdisc->refcnt))return ; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc); }EXPORT_SYMBOL (qdisc_put);
@@ -1094,7 +1102,7 @@ void qdisc_put_unlocked(struct Qdisc *qdisc) !refcount_dec_and_rtnl_lock(&qdisc->refcnt)) return; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc);rtnl_unlock (); }EXPORT_SYMBOL (qdisc_put_unlocked);
Signature · 1 line
2.20.1
Re: [PATCH net 1/6] net/sched: sch_ingress: Only create under TC_H_INGRESS
In reply to Peilin YeQuoted 9 lines · Peilin Ye
ingress Qdiscs are only supposed to be created under
TC_H_INGRESS. Similar tomq_init(), return-EOPNOTSUPPif 'parent' is notTC_H_INGRESS.Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")Reported-by [email protected]Signed-off-by Peilin Ye <[email protected]>
cheers,
jamal
Quoted 21 lines
--- net/sched/sch_ingress.c | 3 +++ 1 file changed, 3 insertions(+)net/sched/sch_ingress.c +3
-1@@ -80,6 +80,9 @@ static int ingress_init(struct Qdisc *sch, struct nlattr *opt,struct net_device *dev =qdisc_dev (sch);int err; +if (sch->parent !=TC_H_INGRESS ) +return -EOPNOTSUPP ; +net_inc_ingress_queue ();mini_qdisc_pair_init (&q->miniqp, sch, &dev->miniq_ingress); --2.20.1
Re: [PATCH net 2/6] net/sched: sch_clsact: Only create under TC_H_CLSACT
In reply to Peilin YeQuoted 7 lines · Peilin Ye
clsact Qdiscs are only supposed to be created under
TC_H_CLSACT(which equalsTC_H_INGRESS). Return-EOPNOTSUPPif 'parent' is notTC_H_CLSACT.Fixes 1f211a1b929c ("net, sched: add clsact qdisc")Signed-off-by Peilin Ye <[email protected]>
cheers,
jamal
Quoted 21 lines
--- net/sched/sch_ingress.c | 3 +++ 1 file changed, 3 insertions(+)net/sched/sch_ingress.c +3
-1@@ -222,6 +222,9 @@ static int clsact_init(struct Qdisc *sch, struct nlattr *opt,struct net_device *dev =qdisc_dev (sch);int err; +if (sch->parent !=TC_H_CLSACT ) +return -EOPNOTSUPP ; +net_inc_ingress_queue ();net_inc_egress_queue (); --2.20.1
Re: [PATCH net 3/6] net/sched: Reserve TC_H_INGRESS (TC_H_CLSACT) for ingress (clsact) Qdiscs
In reply to Peilin YeQuoted 17 lines · Peilin Ye
Currently it is possible to add e.g. an HTB Qdisc under ffff:fff1 (
TC_H_INGRESS,TC_H_CLSACT):$ ip link add name ifb0 type ifb $ tc qdisc add dev ifb0 parent ffff:fff1 htb $ tc qdisc add dev ifb0 clsact Error: Exclusivity flag on, cannot modify. $ drgn ...Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
ifb0 =
netdev_get_by_name(prog, "ifb0")
qdisc = ifb0.ingress_queue.qdisc_sleeping
print(qdisc.ops.id.string_().decode())htb
Quoted 1 line
Quoted 1 line
Quoted 1 line
qdisc.flags.value_()#TCQ_F_INGRESS2
Quoted 7 lines
Only allow ingress and clsact Qdiscs under ffff:fff1. Return
-EINVALfor everything else. MakeTCQ_F_INGRESSa static flag of ingress and clsact Qdiscs.Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")Fixes 1f211a1b929c ("net, sched: add clsact qdisc")Signed-off-by Peilin Ye <[email protected]>
cheers,
jamal
Quoted 48 lines
--- net/sched/sch_api.c | 7 ++++++- net/sched/sch_ingress.c | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-)net/sched/sch_api.c +6
-1@@ -1252,7 +1252,12 @@ static struct Qdisc *qdisc_create(struct net_device *dev, sch->parent = parent;if (handle ==TC_H_INGRESS ) { - sch->flags |=TCQ_F_INGRESS ; +if (!(sch->flags &TCQ_F_INGRESS )) { +NL_SET_ERR_MSG (extack, +"Specified parent ID is reserved for ingress and clsact Qdiscs" ); + err = -EINVAL ; + goto err_out3; + } handle =TC_H_MAKE (TC_H_INGRESS ,0 ); }else {if (handle ==0 ) {net/sched/sch_ingress.c +2
-3@@ -137,7 +137,7 @@ static struct Qdisc_ops ingress_qdisc_ops __read_mostly = { .cl_ops = &ingress_class_ops, .id = "ingress", .priv_size = sizeof(struct ingress_sched_data), - .static_flags = TCQ_F_CPUSTATS, + .static_flags = TCQ_F_INGRESS | TCQ_F_CPUSTATS, .init = ingress_init, .destroy = ingress_destroy, .dump = ingress_dump,@@ -275,7 +275,7 @@ static struct Qdisc_ops clsact_qdisc_ops __read_mostly = { .cl_ops = &clsact_class_ops, .id = "clsact", .priv_size = sizeof(struct clsact_sched_data), - .static_flags = TCQ_F_CPUSTATS, + .static_flags = TCQ_F_INGRESS | TCQ_F_CPUSTATS, .init = clsact_init, .destroy = clsact_destroy, .dump = ingress_dump, --2.20.1
Re: [PATCH net 4/6] net/sched: Prohibit regrafting ingress or clsact Qdiscs
In reply to Peilin YeQuoted 17 lines · Peilin Ye
Currently, after creating an ingress (or clsact) Qdisc and grafting it under
TC_H_INGRESS(TC_H_CLSACT), it is possible to graft it again under e.g. a TBF Qdisc:$ ip link add ifb0 type ifb $ tc qdisc add dev ifb0 handle 1: root tbf rate 20kbit buffer 1600 limit 3000 $ tc qdisc add dev ifb0 clsact $ tc qdisc link dev ifb0 handle ffff: parent 1:1 $ tc qdisc show dev ifb0 qdisc tbf 1: root refcnt 2 rate 20Kbit burst 1600b lat 560.0ms qdisc clsact ffff: parent ffff:fff1 refcnt 2 ^^^^^^^^clsact's refcount has increased: it is now grafted under both
TC_H_CLSACTand 1:1.
Quoted 6 lines
ingress and clsact Qdiscs should only be used under
TC_H_INGRESS(TC_H_CLSACT). Prohibit regrafting them.Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")Fixes 1f211a1b929c ("net, sched: add clsact qdisc")Signed-off-by Peilin Ye <[email protected]>
cheers,
jamal
Quoted 23 lines
--- net/sched/sch_api.c | 5 +++++ 1 file changed, 5 insertions(+)net/sched/sch_api.c +5
-1@@ -1596,6 +1596,11 @@ static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n,NL_SET_ERR_MSG (extack,"Invalid qdisc name" );return -EINVAL ; } +if (q->flags &TCQ_F_INGRESS ) { +NL_SET_ERR_MSG (extack, +"Cannot regraft ingress or clsact Qdiscs" ); +return -EINVAL ; + }if (q == p || (p &&check_loop (q, p,0 ))) {NL_SET_ERR_MSG (extack,"Qdisc parent/child loop detected" ); --2.20.1
Re: [PATCH net 5/6] net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs
In reply to Peilin YeQuoted 5 lines · Peilin Ye
Grafting ingress and clsact Qdiscs does not need a for-loop in
qdisc_graft(). Refactor it. No functional changes intended.Signed-off-by Peilin Ye <[email protected]>
Fixed John's email address.
This one i am not so sure; num_q = 1 implies it will run on the for loop only once. I am not sure it improves readability either. Anyways for the effort you put into it i am tossing a coin and saying:
cheers,
jamal
Quoted 52 lines
net/sched/sch_api.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-)net/sched/sch_api.c +10
-11@@ -1073,12 +1073,12 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (parent ==NULL ) {unsigned int i, num_q, ingress; +struct netdev_queue *dev_queue; ingress =0 ; num_q = dev->num_tx_queues;if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { - num_q =1 ; ingress =1 ;if (!dev_ingress_queue (dev)) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );@@ -1094,18 +1094,18 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach && !ingress) goto skip; -for (i =0 ; i < num_q; i++) { -struct netdev_queue *dev_queue =dev_ingress_queue (dev); - -if (!ingress) +if (!ingress) { +for (i =0 ; i < num_q; i++) { dev_queue =netdev_get_tx_queue (dev, i); + old =dev_graft_qdisc (dev_queue, new); - old =dev_graft_qdisc (dev_queue, new); -if (new && i >0 ) -qdisc_refcount_inc (new); - -if (!ingress) +if (new && i >0 ) +qdisc_refcount_inc (new);qdisc_put (old); + } + }else { + dev_queue =dev_ingress_queue (dev); + old =dev_graft_qdisc (dev_queue, new); } skip: --2.20.1
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Peilin YeQuoted 83 lines · Peilin Ye
mini_Qdisc_pair::p_miniqis a double pointer tomini_Qdisc, initialized iningress_init()to point tonet_device::miniq_ingress. ingress Qdiscs access this per-net_device pointer inmini_qdisc_pair_swap(). Similar for clsact Qdiscs andminiq_egress.Unfortunately, after introducing RTNL-lockless RTM_{NEW,DEL,GET}TFILTER requests, when e.g. replacing ingress (clsact) Qdiscs, the old Qdisc could access the same miniq_{in,e}gress pointer(s) concurrently with the new Qdisc, causing race conditions [1] including a use-after-free in
mini_qdisc_pair_swap()reported by syzbot:BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 ... Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266...
The new (ingress or clsact) Qdisc should only call
mini_qdisc_pair_swap()after the old Qdisc's last call (in {ingress,clsact}_destroy()) has finished.To achieve this, in
qdisc_graft(), return-EBUSYif the old (ingress or clsact) Qdisc has ongoing RTNL-lockless filter requests, and callqdisc_destroy()for "old" before grafting "new".Introduce
qdisc_refcount_dec_if_one()as the counterpart ofqdisc_refcount_inc_nz()used for RTNL-lockless filter requests. Introduce a non-static version ofqdisc_destroy()that does aTCQ_F_BUILTINcheck, just likeqdisc_put()etc.[1] To illustrate, the syzkaller reproducer adds ingress Qdiscs under
TC_H_ROOT(no longer possible after patch "net/sched:sch_ingress: Only create underTC_H_INGRESS") on eth0 that has 8 transmission queues:Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, RTNL-locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, RTNL-lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A)(1st)
|
|RTM_NEWQDISC(B, RTNL-locked)
RCU 2qdisc_graft(B)
| 1notify_and_destroy(A)
|
tcf_block_release(A)0RTM_NEWTFILTER(Y, RTNL-lockless)
qdisc_destroy(A)tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A)mini_qdisc_pair_swap(B)(2nd)
mini_qdisc_pair_swap(A)(3rd) |
... ...Here, B calls
mini_qdisc_pair_swap(), pointingeth0->miniq_ingressto its mini Qdisc, b1. Then, A callsmini_qdisc_pair_swap()again duringingress_destroy(), settingeth0->miniq_ingressto NULL, so ingress packets on eth0 will not find filter Y insch_handle_ingress().This is just one of the possible consequences of concurrently accessing net_device::miniq_{in,e}gress pointers. The point is clear, however:
B's first call tomini_qdisc_pair_swap()should take place after A's last call, inqdisc_destroy().
Quoted 6 lines
Fixes 7a096d579e8e ("net: sched: ingress: set 'unlocked' flag for Qdisc ops")Fixes 87f373921c4e ("net: sched: ingress: set 'unlocked' flag for clsact Qdisc ops")Reported-by [email protected]Cc Hillf Danton <[email protected]>Signed-off-by Peilin Ye <[email protected]>
Thanks for the excellent analysis Peilin and for chasing this to the end. I have no doubt it was a lot of fun! ;->
cheers,
jamal
Quoted 135 lines
--- include/net/sch_generic.h | 8 ++++++++ net/sched/sch_api.c | 26 +++++++++++++++++++++----- net/sched/sch_generic.c | 14 +++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-)include/net/sch_generic.h +8
-0@@ -137,6 +137,13 @@ static inline void qdisc_refcount_inc(struct Qdisc *qdisc)refcount_inc (&qdisc->refcnt); } +static inline bool qdisc_refcount_dec_if_one (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return true; +return refcount_dec_if_one (&qdisc->refcnt); +} +/* Intended to be used by unlocked users, when concurrent qdisc release is * possible. */ @@ -652,6 +659,7 @@ void dev_deactivate_many(struct list_head *head);struct Qdisc *dev_graft_qdisc (struct netdev_queue *dev_queue,struct Qdisc *qdisc);void qdisc_reset (struct Qdisc *qdisc); +void qdisc_destroy (struct Qdisc *qdisc);void qdisc_put (struct Qdisc *qdisc);void qdisc_put_unlocked (struct Qdisc *qdisc);void qdisc_tree_reduce_backlog (struct Qdisc *qdisc,int n,int len);net/sched/sch_api.c +21
-5@@ -1080,10 +1080,20 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { ingress =1 ; -if (!dev_ingress_queue (dev)) { + dev_queue =dev_ingress_queue (dev); +if (!dev_queue) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );return -ENOENT ; } + +/* This is the counterpart of that qdisc_refcount_inc_nz() call in +* __tcf_qdisc_find() for RTNL-lockless filter requests. +*/ +if (!qdisc_refcount_dec_if_one (dev_queue->qdisc_sleeping)) { +NL_SET_ERR_MSG (extack, +"Current ingress or clsact Qdisc has ongoing filter request(s)" ); +return -EBUSY ; + } }if (dev->flags &IFF_UP )@@ -1104,8 +1114,16 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,qdisc_put (old); } }else { - dev_queue =dev_ingress_queue (dev); - old =dev_graft_qdisc (dev_queue, new); + old =dev_graft_qdisc (dev_queue,NULL ); + +/* {ingress,clsact}_destroy() "old" before grafting "new" to avoid +* unprotected concurrent accesses to net_device::miniq_{in,e}gress +* pointer(s) in mini_qdisc_pair_swap(). +*/ +qdisc_notify (net, skb, n, classid, old, new, extack); +qdisc_destroy (old); + +dev_graft_qdisc (dev_queue, new); } skip:@@ -1119,8 +1137,6 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach) new->ops->attach (new); - }else { -notify_and_destroy (net, skb, n, classid, old, new, extack); }if (dev->flags &IFF_UP )net/sched/sch_generic.c +11
-4@@ -1046,7 +1046,7 @@ static void qdisc_free_cb(struct rcu_head *head)qdisc_free (q); } -static void qdisc_destroy (struct Qdisc *qdisc) +static void __qdisc_destroy (struct Qdisc *qdisc) {const struct Qdisc_ops *ops = qdisc->ops;@@ -1070,6 +1070,14 @@ static void qdisc_destroy(struct Qdisc *qdisc)call_rcu (&qdisc->rcu, qdisc_free_cb); } +void qdisc_destroy (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return ; + +__qdisc_destroy (qdisc); +} +void qdisc_put (struct Qdisc *qdisc) {if (!qdisc)@@ -1079,7 +1087,7 @@ void qdisc_put(struct Qdisc *qdisc) !refcount_dec_and_test(&qdisc->refcnt))return ; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc); }EXPORT_SYMBOL (qdisc_put);@@ -1094,7 +1102,7 @@ void qdisc_put_unlocked(struct Qdisc *qdisc) !refcount_dec_and_rtnl_lock(&qdisc->refcnt))return ; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc);rtnl_unlock (); }EXPORT_SYMBOL (qdisc_put_unlocked); --2.20.1
Re: [PATCH net 5/6] net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs
In reply to Peilin YeQuoted 4 lines · Peilin Ye
Grafting ingress and clsact Qdiscs does not need a for-loop in
qdisc_graft(). Refactor it. No functional changes intended.Signed-off-by Peilin Ye <[email protected]>
Just a FYI:
If you decide to keep this refactoring, it will need to be back ported together with the subsequent fix.
I would personally leave it to net-next.
Thanks for chasing this!
Quoted 50 lines
--- net/sched/sch_api.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-)net/sched/sch_api.c +10
-10@@ -1073,12 +1073,12 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (parent ==NULL ) {unsigned int i, num_q, ingress; +struct netdev_queue *dev_queue; ingress =0 ; num_q = dev->num_tx_queues;if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { - num_q =1 ; ingress =1 ;if (!dev_ingress_queue (dev)) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );@@ -1094,18 +1094,18 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach && !ingress) goto skip; -for (i =0 ; i < num_q; i++) { -struct netdev_queue *dev_queue =dev_ingress_queue (dev); - -if (!ingress) +if (!ingress) { +for (i =0 ; i < num_q; i++) { dev_queue =netdev_get_tx_queue (dev, i); + old =dev_graft_qdisc (dev_queue, new); - old =dev_graft_qdisc (dev_queue, new); -if (new && i >0 ) -qdisc_refcount_inc (new); - -if (!ingress) +if (new && i >0 ) +qdisc_refcount_inc (new);qdisc_put (old); + } + }else { + dev_queue =dev_ingress_queue (dev); + old =dev_graft_qdisc (dev_queue, new); } skip:
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Peilin YeQuoted 82 lines · Peilin Ye
mini_Qdisc_pair::p_miniqis a double pointer tomini_Qdisc, initialized iningress_init()to point tonet_device::miniq_ingress. ingress Qdiscs access this per-net_device pointer inmini_qdisc_pair_swap(). Similar for clsact Qdiscs andminiq_egress.Unfortunately, after introducing RTNL-lockless RTM_{NEW,DEL,GET}TFILTER requests, when e.g. replacing ingress (clsact) Qdiscs, the old Qdisc could access the same miniq_{in,e}gress pointer(s) concurrently with the new Qdisc, causing race conditions [1] including a use-after-free in
mini_qdisc_pair_swap()reported by syzbot:BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 ... Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266...
The new (ingress or clsact) Qdisc should only call
mini_qdisc_pair_swap()after the old Qdisc's last call (in {ingress,clsact}_destroy()) has finished.To achieve this, in
qdisc_graft(), return-EBUSYif the old (ingress or clsact) Qdisc has ongoing RTNL-lockless filter requests, and callqdisc_destroy()for "old" before grafting "new".Introduce
qdisc_refcount_dec_if_one()as the counterpart ofqdisc_refcount_inc_nz()used for RTNL-lockless filter requests. Introduce a non-static version ofqdisc_destroy()that does aTCQ_F_BUILTINcheck, just likeqdisc_put()etc.[1] To illustrate, the syzkaller reproducer adds ingress Qdiscs under
TC_H_ROOT(no longer possible after patch "net/sched:sch_ingress: Only create underTC_H_INGRESS") on eth0 that has 8 transmission queues:Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, RTNL-locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, RTNL-lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A)(1st)
|
|RTM_NEWQDISC(B, RTNL-locked)
RCU 2qdisc_graft(B)
| 1notify_and_destroy(A)
|
tcf_block_release(A)0RTM_NEWTFILTER(Y, RTNL-lockless)
qdisc_destroy(A)tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A)mini_qdisc_pair_swap(B)(2nd)
mini_qdisc_pair_swap(A)(3rd) |
... ...Here, B calls
mini_qdisc_pair_swap(), pointingeth0->miniq_ingressto its mini Qdisc, b1. Then, A callsmini_qdisc_pair_swap()again duringingress_destroy(), settingeth0->miniq_ingressto NULL, so ingress packets on eth0 will not find filter Y insch_handle_ingress().This is just one of the possible consequences of concurrently accessing net_device::miniq_{in,e}gress pointers. The point is clear, however:
B's first call tomini_qdisc_pair_swap()should take place after A's last call, inqdisc_destroy().
Quoted 6 lines
Fixes 7a096d579e8e ("net: sched: ingress: set 'unlocked' flag for Qdisc ops")Fixes 87f373921c4e ("net: sched: ingress: set 'unlocked' flag for clsact Qdisc ops")Reported-by [email protected]Cc Hillf Danton <[email protected]>Signed-off-by Peilin Ye <[email protected]>
Thanks for chasing this!
Quoted 132 lines
--- include/net/sch_generic.h | 8 ++++++++ net/sched/sch_api.c | 26 +++++++++++++++++++++----- net/sched/sch_generic.c | 14 +++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-)include/net/sch_generic.h +8
-0@@ -137,6 +137,13 @@ static inline void qdisc_refcount_inc(struct Qdisc *qdisc)refcount_inc (&qdisc->refcnt); } +static inline bool qdisc_refcount_dec_if_one (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return true; +return refcount_dec_if_one (&qdisc->refcnt); +} +/* Intended to be used by unlocked users, when concurrent qdisc release is * possible. */ @@ -652,6 +659,7 @@ void dev_deactivate_many(struct list_head *head);struct Qdisc *dev_graft_qdisc (struct netdev_queue *dev_queue,struct Qdisc *qdisc);void qdisc_reset (struct Qdisc *qdisc); +void qdisc_destroy (struct Qdisc *qdisc);void qdisc_put (struct Qdisc *qdisc);void qdisc_put_unlocked (struct Qdisc *qdisc);void qdisc_tree_reduce_backlog (struct Qdisc *qdisc,int n,int len);net/sched/sch_api.c +21
-5@@ -1080,10 +1080,20 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { ingress =1 ; -if (!dev_ingress_queue (dev)) { + dev_queue =dev_ingress_queue (dev); +if (!dev_queue) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );return -ENOENT ; } + +/* This is the counterpart of that qdisc_refcount_inc_nz() call in +* __tcf_qdisc_find() for RTNL-lockless filter requests. +*/ +if (!qdisc_refcount_dec_if_one (dev_queue->qdisc_sleeping)) { +NL_SET_ERR_MSG (extack, +"Current ingress or clsact Qdisc has ongoing filter request(s)" ); +return -EBUSY ; + } }if (dev->flags &IFF_UP )@@ -1104,8 +1114,16 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,qdisc_put (old); } }else { - dev_queue =dev_ingress_queue (dev); - old =dev_graft_qdisc (dev_queue, new); + old =dev_graft_qdisc (dev_queue,NULL ); + +/* {ingress,clsact}_destroy() "old" before grafting "new" to avoid +* unprotected concurrent accesses to net_device::miniq_{in,e}gress +* pointer(s) in mini_qdisc_pair_swap(). +*/ +qdisc_notify (net, skb, n, classid, old, new, extack); +qdisc_destroy (old); + +dev_graft_qdisc (dev_queue, new); } skip:@@ -1119,8 +1137,6 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach) new->ops->attach (new); - }else { -notify_and_destroy (net, skb, n, classid, old, new, extack); }if (dev->flags &IFF_UP )net/sched/sch_generic.c +11
-3@@ -1046,7 +1046,7 @@ static void qdisc_free_cb(struct rcu_head *head)qdisc_free (q); } -static void qdisc_destroy (struct Qdisc *qdisc) +static void __qdisc_destroy (struct Qdisc *qdisc) {const struct Qdisc_ops *ops = qdisc->ops;@@ -1070,6 +1070,14 @@ static void qdisc_destroy(struct Qdisc *qdisc)call_rcu (&qdisc->rcu, qdisc_free_cb); } +void qdisc_destroy (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return ; + +__qdisc_destroy (qdisc); +} +void qdisc_put (struct Qdisc *qdisc) {if (!qdisc)@@ -1079,7 +1087,7 @@ void qdisc_put(struct Qdisc *qdisc) !refcount_dec_and_test(&qdisc->refcnt))return ; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc); }EXPORT_SYMBOL (qdisc_put);@@ -1094,7 +1102,7 @@ void qdisc_put_unlocked(struct Qdisc *qdisc) !refcount_dec_and_rtnl_lock(&qdisc->refcnt)) return; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc);rtnl_unlock (); }EXPORT_SYMBOL (qdisc_put_unlocked);
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Jamal Hadi SalimQuoted 2 lines · Jamal Hadi Salim
Thanks for the excellent analysis Peilin and for chasing this to the end. I have no doubt it was a lot of fun! ;->
Of course ;-)
Quoted 2 lines
Reviewed-by Jamal Hadi Salim <[email protected]>Acked-by Jamal Hadi Salim <[email protected]>
Thanks for reviewing this!
Peilin Ye
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Pedro TammelaQuoted 3 lines · Pedro Tammela
Thanks for chasing this!
Tested-by Pedro Tammela <[email protected]>
Thanks for testing, Pedro!
Thanks,
Peilin Ye
Re: [PATCH net 5/6] net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs
In reply to Jamal Hadi SalimQuoted 10 lines · Jamal Hadi Salim
Quoted 3 lines · Peilin Ye
Grafting ingress and clsact Qdiscs does not need a for-loop in
qdisc_graft(). Refactor it. No functional changes intended.This one i am not so sure;
num_q= 1 implies it will run on the for loop only once. I am not sure it improves readability either. Anyways for the effort you put into it i am tossing a coin and saying:Reviewed-by Jamal Hadi Salim <[email protected]>Acked-by Jamal Hadi Salim <[email protected]>
Yeah, it doesn't make much difference itself. I'm just afraid that, without [5/6], [6/6] would look like:
for (i = 0; i < num_q; i++) {
if (!ingress) {
dev_queue = netdev_get_tx_queue(dev, i);
old = dev_graft_qdisc(dev_queue, new);
else {
old = dev_graft_qdisc(dev_queue, NULL);
}
if (new && i > 0)
qdisc_refcount_inc(new);
if (!ingress) {
qdisc_put(old);
} else {
/* {ingress,clsact}_destroy() "old" before grafting "new" to avoid
* unprotected concurrent accesses to net_device::miniq_{in,e}gress
* pointer(s) in mini_qdisc_pair_swap().
*/
qdisc_notify(net, skb, n, classid, old, new, extack);
qdisc_destroy(old);
}
if (ingress)
dev_graft_qdisc(dev_queue, new);
}The "!ingress" path doesn't share a single line with "ingress", which looks a bit weird to me. Personally I'd like to keep [5/6].
Thanks,
Peilin Ye
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Peilin YeQuoted 31 lines · On Fri, 5 May 2023 17:16:10 -0700 Peilin Ye wrote:
Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, RTNL-locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, RTNL-lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A)(1st)
|
|RTM_NEWQDISC(B, RTNL-locked)
RCU 2qdisc_graft(B)
| 1notify_and_destroy(A)
|
tcf_block_release(A)0RTM_NEWTFILTER(Y, RTNL-lockless)
qdisc_destroy(A)tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A)mini_qdisc_pair_swap(B)(2nd)
mini_qdisc_pair_swap(A)(3rd) |
... ...Here, B calls
mini_qdisc_pair_swap(), pointingeth0->miniq_ingressto its mini Qdisc, b1. Then, A callsmini_qdisc_pair_swap()again duringingress_destroy(), settingeth0->miniq_ingressto NULL, so ingress packets on eth0 will not find filter Y insch_handle_ingress().
Quoted 4 lines
This is just one of the possible consequences of concurrently accessing net_device::miniq_{in,e}gress pointers. The point is clear, however:
B's first call tomini_qdisc_pair_swap()should take place after A's last call, inqdisc_destroy().
Great analysis, thanks for squashing this bug.
Have you considered creating a fix more localized to the miniq implementation? It seems that having per-device miniq pointers is incompatible with using reference counted objects. So miniq is a more natural place to solve the problem. Otherwise workarounds in the core keep piling up (here qdisc_graft()).
Can we replace the rcu_assign_pointer in (3rd) with a cmpxchg()? If active qdisc is neither a1 nor a2 we should leave the dev state alone.
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 1 line · Jakub Kicinski
Great analysis, thanks for squashing this bug.
Thanks, happy to help!
Quoted 9 lines
Have you considered creating a fix more localized to the miniq implementation? It seems that having per-device miniq pointers is incompatible with using reference counted objects. So miniq is a more natural place to solve the problem. Otherwise workarounds in the core keep piling up (here
qdisc_graft()).Can we replace the
rcu_assign_pointerin (3rd) with acmpxchg()? If active qdisc is neither a1 nor a2 we should leave the dev state alone.
Yes, I have tried fixing this in mini_qdisc_pair_swap(), but I am afraid it is hard:
(3rd) is called from ->destroy(), so currently it uses RCU_INIT_POINTER() to set dev->miniq_ingress to NULL. It will need a logic like:
I am A. Set dev->miniq_ingress to NULL, if and only if it is a1 or a2, and do it atomically.
We need more than a cmpxchg() to implement this "set NULL iff a1 or a2". Additionally:
Quoted 11 lines · On Fri, 5 May 2023 17:16:10 -0700 Peilin Ye wrote:
Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, RTNL-locked)
qdisc_create(A)1
qdisc_graft(A)9
Quoted 14 lines
RTM_NEWTFILTER(X, RTNL-lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A)(1st)
|
|RTM_NEWQDISC(B, RTNL-locked)
RCU 2qdisc_graft(B)
| 1notify_and_destroy(A)
|
tcf_block_release(A)0RTM_NEWTFILTER(Y, RTNL-lockless)
qdisc_destroy(A)tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A)mini_qdisc_pair_swap(B)(2nd)
mini_qdisc_pair_swap(A)(3rd) |
... ...
Looking at the code, I think there is no guarantee that (1st) cannot happen after (2nd), although unlikely? Can RTNL-lockless RTM_NEWTFILTER handlers get preempted?
If (1st) happens later than (2nd), we will need to make (1st) no-op, by detecting that we are the "old" Qdisc. I am not sure there is any (clean) way to do it. I even thought about:
(1) Get the containing Qdisc of "miniqp" we are working on, "qdisc";
(2) Test if "qdisc == qdisc->dev_queue->qdisc_sleeping". If false, it means we are the "old" Qdisc (have been replaced), and should do nothing.
However, for clsact Qdiscs I don't know if "miniqp" is the ingress or egress one, so I can't container_of() during step (1) ...
Eventually I created [5,6/6]. It is a workaround indeed, in the sense that it changes sch_api.c to avoid a mini Qdisc issue. However I think it makes the code correct in a relatively understandable way, without slowing down mini_qdisc_pair_swap() or sch_handle_*gress().
Thanks,
Peilin Ye
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 27 lines · On Wed, 10 May 2023 13:11:19 -0700 Peilin Ye wrote:
Quoted 11 lines · On Fri, 5 May 2023 17:16:10 -0700 Peilin Ye wrote:
Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, RTNL-locked)
qdisc_create(A)1
qdisc_graft(A)9Quoted 14 lines
RTM_NEWTFILTER(X, RTNL-lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A)(1st)
|
|RTM_NEWQDISC(B, RTNL-locked)
RCU 2qdisc_graft(B)
| 1notify_and_destroy(A)
|
tcf_block_release(A)0RTM_NEWTFILTER(Y, RTNL-lockless)
qdisc_destroy(A)tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A)mini_qdisc_pair_swap(B)(2nd)
mini_qdisc_pair_swap(A)(3rd) |
... ...
Quoted 3 lines
Looking at the code, I think there is no guarantee that (1st) cannot happen after (2nd), although unlikely? Can RTNL-lockless
RTM_NEWTFILTERhandlers get preempted?
Right, we need qdisc_graft(B) to update the appropriate dev pointer to point to b1. With that the ordering should not matter. Probably using the ->attach() callback?
Quoted 11 lines
If (1st) happens later than (2nd), we will need to make (1st) no-op, by detecting that we are the "old" Qdisc. I am not sure there is any (clean) way to do it. I even thought about:
(1) Get the containing Qdisc of "miniqp" we are working on, "qdisc";
(2) Test if "qdisc ==qdisc->dev_queue->qdisc_sleeping". If false, it means we are the "old" Qdisc (have been replaced), and should do nothing.However, for clsact Qdiscs I don't know if "miniqp" is the ingress or egress one, so I can't
container_of()during step (1) ...
And we can't be using multiple pieces of information to make the decision since AFAIU mini_qdisc_pair_swap() can race with qdisc_graft().
My thinking was to make sure that dev->miniq_* pointers always point to one of the miniqs of the currently attached qdisc. Right now, on a quick look, those pointers are not initialized during initial graft, only when first filter is added, and may be cleared when filters are removed. But I don't think that's strictly required, miniq with no filters should be fine.
Quoted 3 lines
Eventually I created [5,6/6]. It is a workaround indeed, in the sense that it changes
sch_api.cto avoid a mini Qdisc issue. However I think it makes the code correct in a relatively understandable way,
What's your benchmark for being understandable?
Quoted 1 line
without slowing down
mini_qdisc_pair_swap()or sch_handle_*gress().
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 6 lines · Jakub Kicinski
My thinking was to make sure that dev->miniq_* pointers always point to one of the miniqs of the currently attached qdisc. Right now, on a quick look, those pointers are not initialized during initial graft, only when first filter is added, and may be cleared when filters are removed. But I don't think that's strictly required, miniq with no filters should be fine.
Ah, I see, thanks for explaining, I didn't think of that. Looking at sch_handle_ingress() BTW, currently mini Qdisc stats aren't being updated (mini_qdisc_bstats_cpu_update()) if there's no filters, is this intended? Should I keep this behavior by:
net/core/dev.c +1 -1
@@ -5116,7 +5116,7 @@ sch_handle_ingress(struct sk_buff *skb, struct packet_type **pt_prev, int *ret,* that are not configured with an ingress qdisc will bail * out here. */ -if (!miniq) +if (!miniq || !miniq->filter_list)return skb;if (*pt_prev) {
Quoted 22 lines · Jakub Kicinski
Quoted 21 lines · On Wed, 10 May 2023 13:11:19 -0700 Peilin Ye wrote:
Quoted 19 lines · On Fri, 5 May 2023 17:16:10 -0700 Peilin Ye wrote:
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC(A, RTNL-locked)
qdisc_create(A)1
qdisc_graft(A)9
RTM_NEWTFILTER(X, RTNL-lockless)
__tcf_qdisc_find(A)10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A)(1st)
|
|RTM_NEWQDISC(B, RTNL-locked)
RCU 2qdisc_graft(B)
| 1notify_and_destroy(A)
|
tcf_block_release(A)0RTM_NEWTFILTER(Y, RTNL-lockless)
qdisc_destroy(A)tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A)mini_qdisc_pair_swap(B)(2nd)
mini_qdisc_pair_swap(A)(3rd) |
... ...
Quoted 7 lines
Quoted 3 lines
Looking at the code, I think there is no guarantee that (1st) cannot happen after (2nd), although unlikely? Can RTNL-lockless
RTM_NEWTFILTERhandlers get preempted?Right, we need
qdisc_graft(B)to update the appropriate dev pointer to point to b1. With that the ordering should not matter. Probably using the ->attach() callback?
->attach() is later than dev_graft_qdisc(). We should get ready for concurrent filter requests (i.e. have dev pointer pointing to b1) before grafting (publishing) B. Also currently qdisc_graft() doesn't call ->attach() for ingress and clsact Qdiscs.
But I see your point, thanks for the suggestion! I'll try ->init() and create v2.
Thanks,
Peilin Ye
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 18 lines · On Thu, 11 May 2023 13:40:10 -0700 Peilin Ye wrote:
Quoted 6 lines · Jakub Kicinski
My thinking was to make sure that dev->miniq_* pointers always point to one of the miniqs of the currently attached qdisc. Right now, on a quick look, those pointers are not initialized during initial graft, only when first filter is added, and may be cleared when filters are removed. But I don't think that's strictly required, miniq with no filters should be fine.
Ah, I see, thanks for explaining, I didn't think of that. Looking at
sch_handle_ingress()BTW, currently mini Qdisc stats aren't being updated (mini_qdisc_bstats_cpu_update()) if there's no filters, is this intended? Should I keep this behavior by:net/core/dev.c +0
-0
net/core/dev.c sch_handle_ingress(struct sk_buff *skb, struct packet_type **pt_prev, int *ret, · line 5122
Quoted 8 lines
net/core/dev.c +1
-1@@ -5116,7 +5116,7 @@ sch_handle_ingress(struct sk_buff *skb, struct packet_type **pt_prev, int *ret,* that are not configured with an ingress qdisc will bail * out here. */ -if (!miniq) +if (!miniq || !miniq->filter_list)return skb;if (*pt_prev) {
Good question, maybe Jiri or Daniel can answer?
Quoted 6 lines · Jakub Kicinski
Quoted 5 lines · On Wed, 10 May 2023 13:11:19 -0700 Peilin Ye wrote:
Quoted 3 lines
Looking at the code, I think there is no guarantee that (1st) cannot happen after (2nd), although unlikely? Can RTNL-lockless
RTM_NEWTFILTERhandlers get preempted?
Quoted 7 lines
Quoted 3 lines
Right, we need
qdisc_graft(B)to update the appropriate dev pointer to point to b1. With that the ordering should not matter. Probably using the ->attach() callback?->attach() is later than
dev_graft_qdisc(). We should get ready for concurrent filter requests (i.e. have dev pointer pointing to b1) before grafting (publishing) B.
I thought even for "unlocked" filter operations the start of it is under the lock, but the lock gets dropped after qdisc/block are found. I could be misremembering, I haven't looked at the code.
Quoted 5 lines
Also currently
qdisc_graft()doesn't call
->attach() for ingress and clsact Qdiscs.But I see your point, thanks for the suggestion! I'll try ->init() and create v2.
->init() may be too early, aren't there any error points which could prevent the Qdisc from binding after ->init() was called?
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 5 lines · Jakub Kicinski
Quoted 2 lines
But I see your point, thanks for the suggestion! I'll try ->init() and create v2.
->init() may be too early, aren't there any error points which could prevent the Qdisc from binding after ->init() was called?
You're right, it's in qdisc_create(), argh...
Quoted 8 lines · Jakub Kicinski
Quoted 8 lines
Quoted 7 lines
Quoted 3 lines
Looking at the code, I think there is no guarantee that (1st) cannot happen after (2nd), although unlikely? Can RTNL-lockless
RTM_NEWTFILTERhandlers get preempted?Right, we need
qdisc_graft(B)to update the appropriate dev pointer to point to b1. With that the ordering should not matter. Probably using the ->attach() callback?
Quoted 7 lines
Quoted 3 lines
->attach() is later than
dev_graft_qdisc(). We should get ready for concurrent filter requests (i.e. have dev pointer pointing to b1) before grafting (publishing) B.I thought even for "unlocked" filter operations the start of it is under the lock, but the lock gets dropped after qdisc/block are found. I could be misremembering, I haven't looked at the code.
No, f.e. RTM_NEWTFILTER is registered as RTNL_FLAG_DOIT_UNLOCKED, so tc_new_tfilter() starts and calls __tcf_qdisc_find() without RTNL mutex, at least in latest code.
Thinking,
Peilin Ye
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 8 lines · Peilin Ye
Quoted 5 lines · Jakub Kicinski
Quoted 2 lines
But I see your point, thanks for the suggestion! I'll try ->init() and create v2.
->init() may be too early, aren't there any error points which could prevent the Qdisc from binding after ->init() was called?
You're right, it's in
qdisc_create(), argh...
->destroy() is called for all error points between ->init() and dev_graft_qdisc(). I'll try handling it in ->destroy().
Thanks,
Peilin Ye
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 7 lines · Peilin Ye
Quoted 4 lines
Quoted 2 lines
->init() may be too early, aren't there any error points which could prevent the Qdisc from binding after ->init() was called?
You're right, it's in
qdisc_create(), argh...->destroy() is called for all error points between ->init() and
dev_graft_qdisc(). I'll try handling it in ->destroy().
Sorry for any confusion: there is no point at all undoing "setting dev pointer to b1" in ->destroy() because datapath has already been affected.
To summarize, grafting B mustn't fail after setting dev pointer to b1, so ->init() is too early, because e.g. if user requested [1] to create a rate estimator, gen_new_estimator() could fail after ->init() in qdisc_create().
On the other hand, ->attach() is too late because it's later than dev_graft_qdisc(), so concurrent filter requests might see uninitialized dev pointer in theory.
Please suggest; is adding another callback (or calling ->attach()) right before dev_graft_qdisc() for ingress (clsact) Qdiscs too much for this fix?
[1] e.g. $ tc qdisc add dev eth0 estimator 1s 8s clsact
Thanks,
Peilin Ye
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 18 lines · On Mon, 15 May 2023 15:45:15 -0700 Peilin Ye wrote:
Quoted 4 lines · Peilin Ye
Quoted 1 line
You're right, it's in
qdisc_create(), argh...->destroy() is called for all error points between ->init() and
dev_graft_qdisc(). I'll try handling it in ->destroy().Sorry for any confusion: there is no point at all undoing "setting dev pointer to b1" in ->destroy() because datapath has already been affected.
To summarize, grafting B mustn't fail after setting dev pointer to b1, so ->init() is too early, because e.g. if user requested [1] to create a rate estimator,
gen_new_estimator()could fail after ->init() inqdisc_create().On the other hand, ->attach() is too late because it's later than
dev_graft_qdisc(), so concurrent filter requests might see uninitialized dev pointer in theory.
Quoted 5 lines
Please suggest; is adding another callback (or calling ->attach()) right before
dev_graft_qdisc()for ingress (clsact) Qdiscs too much for this fix?[1] e.g. $ tc qdisc add dev eth0 estimator 1s 8s clsact
Vlad, could you please clarify how you expect the unlocked filter operations to work when the qdisc has global state?
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 19 lines · Jakub Kicinski
Quoted 18 lines · On Mon, 15 May 2023 15:45:15 -0700 Peilin Ye wrote:
Quoted 4 lines · Peilin Ye
Quoted 1 line
You're right, it's in
qdisc_create(), argh...->destroy() is called for all error points between ->init() and
dev_graft_qdisc(). I'll try handling it in ->destroy().Sorry for any confusion: there is no point at all undoing "setting dev pointer to b1" in ->destroy() because datapath has already been affected.
To summarize, grafting B mustn't fail after setting dev pointer to b1, so ->init() is too early, because e.g. if user requested [1] to create a rate estimator,
gen_new_estimator()could fail after ->init() inqdisc_create().On the other hand, ->attach() is too late because it's later than
dev_graft_qdisc(), so concurrent filter requests might see uninitialized dev pointer in theory.
Quoted 8 lines
Quoted 5 lines
Please suggest; is adding another callback (or calling ->attach()) right before
dev_graft_qdisc()for ingress (clsact) Qdiscs too much for this fix?[1] e.g. $ tc qdisc add dev eth0 estimator 1s 8s clsact
Vlad, could you please clarify how you expect the unlocked filter operations to work when the qdisc has global state?
Jakub, I didn't account for per-net_device pointer usage by miniqp code hence this bug. I didn't comment on the fix because I was away from my PC last week but Peilin's approach seems reasonable to me. When Peilin brought up the issue initially I also tried to come up with some trick to contain the changes to miniqp code instead of changing core but couldn't think of anything workable due to the limitations already discussed in this thread. I'm open to explore alternative approaches to solving this issue, if that is what you suggest.
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 11 lines · On Tue, 16 May 2023 22:35:51 +0300 Vlad Buslov wrote:
Quoted 2 lines
Vlad, could you please clarify how you expect the unlocked filter operations to work when the qdisc has global state?
Jakub, I didn't account for per-net_device pointer usage by miniqp code hence this bug. I didn't comment on the fix because I was away from my PC last week but Peilin's approach seems reasonable to me. When Peilin brought up the issue initially I also tried to come up with some trick to contain the changes to miniqp code instead of changing core but couldn't think of anything workable due to the limitations already discussed in this thread. I'm open to explore alternative approaches to solving this issue, if that is what you suggest.
Given Peilin's investigation I think fix without changing core may indeed be hard. I'm not sure if returning -EBUSY when qdisc refcnt is elevated will be appreciated by the users, do we already have similar behavior in other parts of TC?
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 12 lines · Jakub Kicinski
Quoted 11 lines
Quoted 2 lines
Vlad, could you please clarify how you expect the unlocked filter operations to work when the qdisc has global state?
Jakub, I didn't account for per-net_device pointer usage by miniqp code hence this bug. I didn't comment on the fix because I was away from my PC last week but Peilin's approach seems reasonable to me. When Peilin brought up the issue initially I also tried to come up with some trick to contain the changes to miniqp code instead of changing core but couldn't think of anything workable due to the limitations already discussed in this thread. I'm open to explore alternative approaches to solving this issue, if that is what you suggest.
Quoted 4 lines
Given Peilin's investigation I think fix without changing core may indeed be hard. I'm not sure if returning
-EBUSYwhen qdisc refcnt is elevated will be appreciated by the users, do we already have similar behavior in other parts of TC?
Seems like trying to delete an "in-use" cls_u32 filter returns -EBUSY:
net/sched/cls_u32.c:
if (ht->refcnt == 1) {
u32_destroy_hnode(tp, ht, extack);
} else {
NL_SET_ERR_MSG_MOD(extack, "Can not delete in-use filter");
return -EBUSY;
}Thanks,
Peilin Ye
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 6 lines · On Tue, 16 May 2023 15:58:46 -0700 Peilin Ye wrote:
Quoted 4 lines
Given Peilin's investigation I think fix without changing core may indeed be hard. I'm not sure if returning
-EBUSYwhen qdisc refcnt is elevated will be appreciated by the users, do we already have similar behavior in other parts of TC?Seems like trying to delete an "in-use"
cls_u32filter returns-EBUSY
I meant -EBUSY due to a race (another operation being in flight). I think that's different.
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 10 lines · Jakub Kicinski
Quoted 6 lines · On Tue, 16 May 2023 15:58:46 -0700 Peilin Ye wrote:
Quoted 4 lines
Given Peilin's investigation I think fix without changing core may indeed be hard. I'm not sure if returning
-EBUSYwhen qdisc refcnt is elevated will be appreciated by the users, do we already have similar behavior in other parts of TC?Seems like trying to delete an "in-use"
cls_u32filter returns-EBUSYI meant
-EBUSYdue to a race (another operation being in flight). I think that's different.
I wonder if somehow leveraging existing tc_modify_qdisc() 'replay' functionality instead of returning error to the user would be a better approach? Currently the function is replayed when qdisc_create() returns EAGAIN. It should be trivial to do the same for qdisc_graft() result.
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 5 lines · On Wed, 17 May 2023 11:49:10 +0300 Vlad Buslov wrote:
Quoted 4 lines · Jakub Kicinski
Quoted 2 lines · On Tue, 16 May 2023 15:58:46 -0700 Peilin Ye wrote:
Seems like trying to delete an "in-use"
cls_u32filter returns-EBUSY
Quoted 7 lines
Quoted 2 lines
I meant
-EBUSYdue to a race (another operation being in flight). I think that's different.I wonder if somehow leveraging existing
tc_modify_qdisc()'replay' functionality instead of returning error to the user would be a better approach? Currently the function is replayed whenqdisc_create()returns EAGAIN. It should be trivial to do the same forqdisc_graft()result.
Sounds better than returning -EBUSY to the user and expecting them to retry, yes.
Jakub Kicinski
maintainerRe: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Peilin Yenet/sched/sch_api.c static int qdisc_graft(struct net_device *dev, struct Qdisc *parent, · line 1126
Quoted 13 lines · On Fri, 5 May 2023 17:16:10 -0700 Peilin Ye wrote:
net/sched/sch_api.c +10
-2}else { - dev_queue =dev_ingress_queue (dev); - old =dev_graft_qdisc (dev_queue, new); + old =dev_graft_qdisc (dev_queue,NULL ); + +/* {ingress,clsact}_destroy() "old" before grafting "new" to avoid +* unprotected concurrent accesses to net_device::miniq_{in,e}gress +* pointer(s) in mini_qdisc_pair_swap(). +*/ +qdisc_notify (net, skb, n, classid, old, new, extack); +qdisc_destroy (old); + +dev_graft_qdisc (dev_queue, new);
BTW can't @old be NULL here?
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
Quoted 7 lines · Jakub Kicinski
Quoted 7 lines
} else {net/sched/sch_api.c +4
-2- dev_queue =dev_ingress_queue (dev); - old =dev_graft_qdisc (dev_queue, new); + old =dev_graft_qdisc (dev_queue,NULL ); + +/* {ingress,clsact}_destroy() "old" before grafting "new" to avoid +* unprotected concurrent accesses to net_device::miniq_{in,e}gress
Quoted 8 lines
net/sched/sch_api.c static int qdisc_graft(struct net_device *dev, struct Qdisc *parent, · line 1126
Quoted 6 lines
net/sched/sch_api.c +6
-0+* pointer(s) in mini_qdisc_pair_swap(). +*/ +qdisc_notify (net, skb, n, classid, old, new, extack); +qdisc_destroy (old); + +dev_graft_qdisc (dev_queue, new);BTW can't @old be NULL here?
ingress_queue->qdisc_sleeping is initialized to &noop_qdisc (placeholder) in dev_ingress_queue_create(), and dev_graft_qdisc() also grafts &noop_qdisc to represent "there's no Qdisc":
/* ... and graft new one */ if (qdisc == NULL) qdisc = &noop_qdisc; dev_queue->qdisc_sleeping = qdisc;
So @old can't be NULL here.
Thanks,
Peilin Ye
Re: [PATCH net 6/6] net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Jakub KicinskiQuoted 8 lines · Jakub Kicinski
Quoted 4 lines · On Wed, 17 May 2023 11:49:10 +0300 Vlad Buslov wrote:
I wonder if somehow leveraging existing
tc_modify_qdisc()'replay' functionality instead of returning error to the user would be a better approach? Currently the function is replayed whenqdisc_create()returns EAGAIN. It should be trivial to do the same forqdisc_graft()result.Sounds better than returning
-EBUSYto the user and expecting them to retry, yes.
Thanks for the suggestion, Vlad! I'll try this in tc_modify_qdisc() and tc_get_qdisc() (for Qdisc deletion) in v2.
Thanks,
Peilin Ye
#syz test: git://gitlab.com/tammela/net.git peilin-patches
Double checking with syzbot
syzbot
syzbotsyzbot tried to test the proposed patch but the build/boot failed:
Hello,
syzbot tried to test the proposed patch but the build/boot failed:
failed to checkout kernel repo git://gitlab.com/tammela/net.git/peilin-patches: failed to run ["git" "fetch" "--force" "71d757925c19d8f23c660d1e07af98f28b9c6977" "peilin-patches"]: exit status 128
fatal: read error: Connection reset by peer
Tested on:
commit: [unknown git tree: git://gitlab.com/tammela/net.git peilin-patches dashboard link: https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b
compiler:
Note: no patches were applied.
#syz test: https://gitlab.com/tammela/net.git peilin-patches Let's try with https then...
syzbot
syzbotsyzbot has tested the proposed patch and the reproducer did not trigger any issue:
Hello,
syzbot has tested the proposed patch and the reproducer did not trigger any issue:
Tested on:
commit: 6078d01d net/sched: qdisc_destroy() old ingress and cl.. git tree: https://gitlab.com/tammela/net.git peilin-patches console output: https://syzkaller.appspot.com/x/log.txt?x=111cf24d280000 kernel config: https://syzkaller.appspot.com/x/.config?x=b22b5699e8595bcd dashboard link: https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b compiler: gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2
Note: no patches were applied.
Note: testing is done by a robot and is best-effort only.