Netdev List
 help / color / mirror / Atom feed
* Re: [EXTERNAL] Re: [PATCH net,v5] virtio_net: clamp rss_max_key_size to NETDEV_RSS_KEY_LEN
From: Jakub Kicinski @ 2026-04-01  1:05 UTC (permalink / raw)
  To: Michael S. Tsirkin
  Cc: Paolo Abeni, Srujana Challa, netdev@vger.kernel.org,
	virtualization@lists.linux.dev, jasowang@redhat.com,
	xuanzhuo@linux.alibaba.com, eperezma@redhat.com,
	davem@davemloft.net, edumazet@google.com, Nithin Kumar Dabilpuram,
	Shiva Shankar Kommula, stable@vger.kernel.org
In-Reply-To: <20260331104737-mutt-send-email-mst@kernel.org>

On Tue, 31 Mar 2026 10:48:41 -0400 Michael S. Tsirkin wrote:
> > > Thank you for the feedback. In net-next, NETDEV_RSS_KEY_LEN is 256. This fix is
> > > also intended for stable kernels, where NETDEV_RSS_KEY_LEN is 52, and
> > > I added the message to make clamping visible in that case.
> > > I will remove the check and send the next version.    
> > 
> > I'm sorry, I haven't looked at the historical context when I wrote my
> > previous reply.
> > 
> > IMHO the additional check does not make sense in the current net tree.
> > On the flip side stable trees will need it. I suggest:
> > 
> > - dropping the check for the 'net' patch
> > - also dropping CC: stable tag
> > - explicitly sending to stable the fix variant including the size check.
> > 
> > @Michael: WDYT?
>
> I was the one who suggested it, the extra check is harmless, I'm
> inclined to always have it.  Less work than maintaining two patches.

Give us an RB tag please and lets close this one? :)

^ permalink raw reply

* Re: [PATCHv3] selftests: Use ktap helpers for runner.sh
From: Hangbin Liu @ 2026-04-01  1:05 UTC (permalink / raw)
  To: Shuah Khan; +Cc: linux-kselftest, Shuah Khan, Brendan Jackman, netdev
In-Reply-To: <014b54f0-c4a6-40f2-afd0-0ea3e2f796e4@linuxfoundation.org>

On Tue, Mar 31, 2026 at 02:51:48PM -0600, Shuah Khan wrote:
> On 2/24/26 18:08, Hangbin Liu wrote:
> > Instead of manually writing ktap messages, we should use the formal
> > ktap helpers in runner.sh. Brendan did some work in d9e6269e3303
> > ("selftests/run_kselftest.sh: exit with error if tests fail") to make
> > run_kselftest.sh exit with the correct return value. However, the output
> > does not include the total results, such as how many tests passed or failed.
> > 
> > Let’s convert all manually printed messages in runner.sh to use the
> > formal ktap helpers. Here are what I changed:
> > 
> >    1. Move TAP header from runner.sh to run_kselftest.sh, since run_kselftest.sh
> >       is the only caller of run_many().
> >    2. In run_kselftest.sh, call run_many() in main process to count the
> >       pass/fail numbers.
> >    3. In run_kselftest.sh, do not generate kselftest_failures_file. Just
> >       use ktap_print_totals to report the result.
> >    4. In runner.sh run_one(), get the return value and use ktap helpers for
> >       all pass/fail reporting. This allows counting pass/fail numbers in the
> >       main process.
> >    5. In runner.sh run_in_netns(), also return the correct rc, so we can
> >       count results during wait.
> > 
> > After the change, the printed result looks like:
> > 
> >    not ok 4 4 selftests: clone3: clone3_cap_checkpoint_restore # exit=1
> >    # Totals: pass:3 fail:1 xfail:0 xpass:0 skip:0 error:0
> > 
> >    ]# echo $?
> >    1
> > 
> > Tested-by: Brendan Jackman <jackmanb@google.com>
> > Signed-off-by: Hangbin Liu <liuhangbin@gmail.com>
> > ---
> > 
> 
> Applied to linux-kselftest next for Linux 7.1-rc1
> 
> I had to fix commit description and long lines in change log.
> In the future make sure to run checkpatch to catch errors
> related to commit descriptions.

Thanks, I will take care of this.

Hangbin
> 
> thanks,
> -- Shuah

^ permalink raw reply

* Re: [net-next v6 09/12] net: bnxt: Add SW GSO completion and teardown support
From: Jakub Kicinski @ 2026-04-01  1:03 UTC (permalink / raw)
  To: Joe Damato
  Cc: netdev, Michael Chan, David S. Miller, Eric Dumazet, Paolo Abeni,
	andrew+netdev, horms, pavan.chebbi, linux-kernel, leon
In-Reply-To: <acw5IWuRiLV0yHVe@devvm20253.cco0.facebook.com>

On Tue, 31 Mar 2026 14:14:09 -0700 Joe Damato wrote:
> > > +	if (!(bp->flags & BNXT_FLAG_UDP_GSO_CAP)) {
> > > +		if (features & NETIF_F_GSO_UDP_L4)
> > > +			bp->tx_wake_thresh = max_t(int, bp->tx_wake_thresh,
> > > +						   BNXT_SW_USO_MAX_DESCS);
> > > +		else
> > > +			bp->tx_wake_thresh = max_t(int, bp->tx_ring_size / 2,
> > > +						   BNXT_MIN_TX_DESC_CNT);  
> > 
> > Adding extra handling for min ring size all over the place looks a bit
> > messy. Can you factor something out of this logic?  
> 
> Could add something like:
> 
>   static int bnxt_min_tx_desc_cnt(struct bnxt *bp)
>   {
>       if (!(bp->flags & BNXT_FLAG_UDP_GSO_CAP) &&
>           (bp->dev->features & NETIF_F_GSO_UDP_L4))
>           return BNXT_SW_USO_MAX_DESCS;
>       return BNXT_MIN_TX_DESC_CNT;
>   }
> 
> and then when setting the tx_wake_thresh, it becomes:
> 
>   bp->tx_wake_thresh = max_t(int, bp->tx_ring_size / 2,
>                              bnxt_min_tx_desc_cnt(bp));
> 
> and fix_features can use the same helper.

Something along these lines looks reasonable.

> Question then is just do we still want to bump BNXT_MIN_TX_DESC_CNT (as per
> your previous comment)?

No, sorry, I sent that one before looking at the relevant part of
the diffs

^ permalink raw reply

* Re: [PATCH net-next v4 1/2] r8152: Add support for 5Gbit Link Speeds and EEE
From: Jakub Kicinski @ 2026-04-01  0:56 UTC (permalink / raw)
  To: Birger Koblitz
  Cc: andrew+netdev, davem, edumazet, pabeni, linux-usb, netdev,
	linux-kernel
In-Reply-To: <50dc7e8c-6c6d-47d1-866e-677638b4deaf@birger-koblitz.de>

On Tue, 31 Mar 2026 17:38:02 +0200 Birger Koblitz wrote:
> > Are there missing switch cases for NWAY_5000M_FULL in the hardware UPS
> > flag configurations, such as r8156_ups_flags?  
> This question has now been raised the third time, see here:
> https://lkml.org/lkml/2026/3/24/1902 and here https://lkml.org/lkml/2026/3/19/1765
> However, while I share the concern, I cannot provide a better answer than
> the first time or second time.

Apologies for the repeats, I tend to send out the full review as
soon as I spot at least one chunk that looks plausible. 
I should do better but re-checking the AI reviews takes so much 
of my time already.. :(

^ permalink raw reply

* [syzbot] [bpf?] [net?] KASAN: slab-use-after-free Read in sk_psock_verdict_data_ready (4)
From: syzbot @ 2026-04-01  0:49 UTC (permalink / raw)
  To: andrii, ast, bpf, daniel, davem, edumazet, horms, jakub,
	john.fastabend, kuba, linux-kernel, netdev, pabeni,
	syzkaller-bugs

Hello,

syzbot found the following issue on:

HEAD commit:    cf0d9080c6f7 Merge branch 'net-hsr-subsystem-cleanups-and-..
git tree:       net-next
console output: https://syzkaller.appspot.com/x/log.txt?x=10eab3d6580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=71d49d824b43a0d9
dashboard link: https://syzkaller.appspot.com/bug?extid=2184232f07e3677fbaef
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8

Unfortunately, I don't have any reproducer for this issue yet.

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/4271eafdaa2c/disk-cf0d9080.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/984de2c215cb/vmlinux-cf0d9080.xz
kernel image: https://storage.googleapis.com/syzbot-assets/e03ead684811/bzImage-cf0d9080.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+2184232f07e3677fbaef@syzkaller.appspotmail.com

==================================================================
BUG: KASAN: slab-use-after-free in sk_psock_verdict_data_ready+0xec/0x590 net/core/skmsg.c:1278
Read of size 8 at addr ffff8880594da860 by task syz.4.1842/11013

CPU: 1 UID: 0 PID: 11013 Comm: syz.4.1842 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 print_address_description mm/kasan/report.c:378 [inline]
 print_report+0xba/0x230 mm/kasan/report.c:482
 kasan_report+0x117/0x150 mm/kasan/report.c:595
 sk_psock_verdict_data_ready+0xec/0x590 net/core/skmsg.c:1278
 unix_stream_sendmsg+0x8a3/0xe80 net/unix/af_unix.c:2482
 sock_sendmsg_nosec net/socket.c:721 [inline]
 __sock_sendmsg net/socket.c:736 [inline]
 ____sys_sendmsg+0x972/0x9f0 net/socket.c:2585
 ___sys_sendmsg+0x2a5/0x360 net/socket.c:2639
 __sys_sendmsg net/socket.c:2671 [inline]
 __do_sys_sendmsg net/socket.c:2676 [inline]
 __se_sys_sendmsg net/socket.c:2674 [inline]
 __x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2674
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7facf899c819
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 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 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007facf9827028 EFLAGS: 00000246 ORIG_RAX: 000000000000002e
RAX: ffffffffffffffda RBX: 00007facf8c15fa0 RCX: 00007facf899c819
RDX: 0000000000000000 RSI: 0000200000000500 RDI: 0000000000000004
RBP: 00007facf8a32c91 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007facf8c16038 R14: 00007facf8c15fa0 R15: 00007ffd41b01c78
 </TASK>

Allocated by task 11013:
 kasan_save_stack mm/kasan/common.c:57 [inline]
 kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
 unpoison_slab_object mm/kasan/common.c:340 [inline]
 __kasan_slab_alloc+0x6c/0x80 mm/kasan/common.c:366
 kasan_slab_alloc include/linux/kasan.h:253 [inline]
 slab_post_alloc_hook mm/slub.c:4538 [inline]
 slab_alloc_node mm/slub.c:4866 [inline]
 kmem_cache_alloc_lru_noprof+0x2b8/0x640 mm/slub.c:4885
 sock_alloc_inode+0x28/0xc0 net/socket.c:316
 alloc_inode+0x6a/0x1b0 fs/inode.c:347
 new_inode_pseudo include/linux/fs.h:3003 [inline]
 sock_alloc net/socket.c:631 [inline]
 __sock_create+0x12d/0x9d0 net/socket.c:1562
 sock_create net/socket.c:1656 [inline]
 __sys_socketpair+0x1c4/0x560 net/socket.c:1803
 __do_sys_socketpair net/socket.c:1856 [inline]
 __se_sys_socketpair net/socket.c:1853 [inline]
 __x64_sys_socketpair+0x9b/0xb0 net/socket.c:1853
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f

Freed by task 15:
 kasan_save_stack mm/kasan/common.c:57 [inline]
 kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
 kasan_save_free_info+0x46/0x50 mm/kasan/generic.c:584
 poison_slab_object mm/kasan/common.c:253 [inline]
 __kasan_slab_free+0x5c/0x80 mm/kasan/common.c:285
 kasan_slab_free include/linux/kasan.h:235 [inline]
 slab_free_hook mm/slub.c:2685 [inline]
 slab_free mm/slub.c:6165 [inline]
 kmem_cache_free+0x187/0x630 mm/slub.c:6295
 rcu_do_batch kernel/rcu/tree.c:2617 [inline]
 rcu_core+0x7cd/0x1070 kernel/rcu/tree.c:2869
 handle_softirqs+0x22a/0x870 kernel/softirq.c:622
 run_ksoftirqd+0x36/0x60 kernel/softirq.c:1063
 smpboot_thread_fn+0x541/0xa50 kernel/smpboot.c:160
 kthread+0x388/0x470 kernel/kthread.c:436
 ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
 ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245

Last potentially related work creation:
 kasan_save_stack+0x3e/0x60 mm/kasan/common.c:57
 kasan_record_aux_stack+0xbd/0xd0 mm/kasan/generic.c:556
 __call_rcu_common kernel/rcu/tree.c:3131 [inline]
 call_rcu+0xee/0x890 kernel/rcu/tree.c:3251
 destroy_inode fs/inode.c:402 [inline]
 evict+0x95b/0xb10 fs/inode.c:870
 __dentry_kill+0x1a2/0x5e0 fs/dcache.c:670
 finish_dput+0xc9/0x480 fs/dcache.c:879
 __fput+0x691/0xa70 fs/file_table.c:477
 task_work_run+0x1d9/0x270 kernel/task_work.c:233
 resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
 __exit_to_user_mode_loop kernel/entry/common.c:67 [inline]
 exit_to_user_mode_loop+0xed/0x480 kernel/entry/common.c:98
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
 do_syscall_64+0x32d/0xf80 arch/x86/entry/syscall_64.c:100
 entry_SYSCALL_64_after_hwframe+0x77/0x7f

The buggy address belongs to the object at ffff8880594da840
 which belongs to the cache sock_inode_cache of size 1344
The buggy address is located 32 bytes inside of
 freed 1344-byte region [ffff8880594da840, ffff8880594dad80)

The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x594d8
head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0
memcg:ffff8880594dfe81
flags: 0xfff00000000040(head|node=0|zone=1|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 00fff00000000040 ffff888140af9dc0 dead000000000100 dead000000000122
raw: 0000000000000000 0000000800160016 00000000f5000000 ffff8880594dfe81
head: 00fff00000000040 ffff888140af9dc0 dead000000000100 dead000000000122
head: 0000000000000000 0000000800160016 00000000f5000000 ffff8880594dfe81
head: 00fff00000000003 ffffea0001653601 00000000ffffffff 00000000ffffffff
head: ffffffffffffffff 0000000000000000 00000000ffffffff 0000000000000008
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 3, migratetype Reclaimable, gfp_mask 0xd20d0(__GFP_RECLAIMABLE|__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 5827, tgid 5827 (syz-executor), ts 157169026983, free_ts 137999783832
 set_page_owner include/linux/page_owner.h:32 [inline]
 post_alloc_hook+0x231/0x280 mm/page_alloc.c:1889
 prep_new_page mm/page_alloc.c:1897 [inline]
 get_page_from_freelist+0x24dc/0x2580 mm/page_alloc.c:3962
 __alloc_frozen_pages_noprof+0x18d/0x380 mm/page_alloc.c:5250
 alloc_slab_page mm/slub.c:3292 [inline]
 allocate_slab+0x77/0x660 mm/slub.c:3481
 new_slab mm/slub.c:3539 [inline]
 refill_objects+0x331/0x3c0 mm/slub.c:7175
 refill_sheaf mm/slub.c:2812 [inline]
 __pcs_replace_empty_main+0x2e6/0x730 mm/slub.c:4615
 alloc_from_pcs mm/slub.c:4717 [inline]
 slab_alloc_node mm/slub.c:4851 [inline]
 kmem_cache_alloc_lru_noprof+0x37c/0x640 mm/slub.c:4885
 sock_alloc_inode+0x28/0xc0 net/socket.c:316
 alloc_inode+0x6a/0x1b0 fs/inode.c:347
 new_inode_pseudo include/linux/fs.h:3003 [inline]
 sock_alloc net/socket.c:631 [inline]
 __sock_create+0x12d/0x9d0 net/socket.c:1562
 sock_create net/socket.c:1656 [inline]
 __sys_socket_create net/socket.c:1693 [inline]
 __sys_socket+0xd6/0x1b0 net/socket.c:1740
 __do_sys_socket net/socket.c:1754 [inline]
 __se_sys_socket net/socket.c:1752 [inline]
 __x64_sys_socket+0x7a/0x90 net/socket.c:1752
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
page last free pid 7484 tgid 7484 stack trace:
 reset_page_owner include/linux/page_owner.h:25 [inline]
 __free_pages_prepare mm/page_alloc.c:1433 [inline]
 __free_frozen_pages+0xc2b/0xdb0 mm/page_alloc.c:2978
 __slab_free+0x263/0x2b0 mm/slub.c:5573
 qlink_free mm/kasan/quarantine.c:163 [inline]
 qlist_free_all+0x97/0x100 mm/kasan/quarantine.c:179
 kasan_quarantine_reduce+0x148/0x160 mm/kasan/quarantine.c:286
 __kasan_slab_alloc+0x22/0x80 mm/kasan/common.c:350
 kasan_slab_alloc include/linux/kasan.h:253 [inline]
 slab_post_alloc_hook mm/slub.c:4538 [inline]
 slab_alloc_node mm/slub.c:4866 [inline]
 kmem_cache_alloc_noprof+0x2bc/0x650 mm/slub.c:4873
 vm_area_alloc+0x24/0x140 mm/vma_init.c:32
 __mmap_new_vma mm/vma.c:2517 [inline]
 __mmap_region mm/vma.c:2759 [inline]
 mmap_region+0x10eb/0x2240 mm/vma.c:2837
 do_mmap+0xc39/0x10c0 mm/mmap.c:559
 vm_mmap_pgoff+0x2c9/0x4f0 mm/util.c:581
 elf_map fs/binfmt_elf.c:400 [inline]
 elf_load+0x248/0x6a0 fs/binfmt_elf.c:423
 load_elf_interp+0x4ce/0xb60 fs/binfmt_elf.c:690
 load_elf_binary+0x1b2f/0x2980 fs/binfmt_elf.c:1255
 search_binary_handler fs/exec.c:1664 [inline]
 exec_binprm fs/exec.c:1696 [inline]
 bprm_execve+0x93d/0x1460 fs/exec.c:1748
 kernel_execve+0x844/0x930 fs/exec.c:1892
 call_usermodehelper_exec_async+0x20f/0x360 kernel/umh.c:109

Memory state around the buggy address:
 ffff8880594da700: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
 ffff8880594da780: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
>ffff8880594da800: fc fc fc fc fc fc fc fc fa fb fb fb fb fb fb fb
                                                       ^
 ffff8880594da880: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
 ffff8880594da900: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================


---
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 syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

^ permalink raw reply

* Re: [PATCH 1/1] net: ipv6: flowlabel: defer exclusive option free until RCU teardown
From: Yuan Tan @ 2026-04-01  0:41 UTC (permalink / raw)
  To: Eric Dumazet, Ren Wei
  Cc: yuantan098, security, netdev, davem, dsahern, kuba, pabeni, horms,
	afaerber, mani, yoshfuji, yifanwucs, tomapufckgml, bird,
	enjou1224z, zcliangcn
In-Reply-To: <CANn89iJ+pDwKy0kKKG8PrSWMXEStgsMP9YGLxwvjcmMJP1PwrA@mail.gmail.com>


On 3/31/2026 1:52 AM, Eric Dumazet wrote:
> On Tue, Mar 31, 2026 at 1:34 AM Ren Wei <n05ec@lzu.edu.cn> wrote:
>> From: Zhengchuan Liang <zcliangcn@gmail.com>
>>
>> `ip6fl_seq_show()` walks the global flowlabel hash under the seq-file
>> RCU read-side lock and prints `fl->opt->opt_nflen` when an option block
>> is present.
> Some points :
>
> Please do not CC security@ if you submit a public patch, there is
> absolutely no reason for it.
>
> Please do not resend the same version within 24 hours; this creates noise.
> Your patch wasn't lost; we have many patch reviews and similar bugs to address.
>
> Thank you.

We sincerely apologize for the confusion caused by our previous email. Previously, when we sent reports and patches to the security list, we were advised that patches should be submitted to netdev@ for public review. To follow this, we are now sending the full vulnerability details to security@ while additionally submitting the patches to netdev@. Please let us know if this is the correct way to handle such cases.

Regarding the CC list, could you provide further guidance on who we should include? Currently, we are CCing everyone suggested by get_maintainer.pl because we previously received feedback about missing relevant maintainers. For public mailing lists, we should only including netdev@. Should we be more selective?

Finally, regarding our tags, our team has a clear division of labor—some focus on finding vulnerabilities, others on fixing them, followed by an internal review and testing before a dedicated member handles community outreach.

We welcome any guidance and will improve our upcoming patches accordingly. To be honest, we are currently a bit uncertain about the exact workflow the community prefers, and we are eager to align our process with your expectations.


^ permalink raw reply

* [PATCH net v2 4/4] net: bcmgenet: relax the xmit ring full case
From: Justin Chen @ 2026-04-01  0:38 UTC (permalink / raw)
  To: netdev
  Cc: pabeni, kuba, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb,
	Justin Chen
In-Reply-To: <20260401003840.3112454-1-justin.chen@broadcom.com>

We have a queue size of 32. If a packet is multiple fragments we can
run through this queue really quickly. Currently we stop the xmit
queue at SKB_FRAG_SIZE which by default can take up half the queue.
Instead lets just look at the incoming packet and freeze the queue
if the incoming packet has more fragments than free_bds. This will
relieve some of the queue timeouts we have been seeing.

Fixes: 1c1008c793fa ("net: bcmgenet: add main driver file")
Signed-off-by: Justin Chen <justin.chen@broadcom.com>
---
 drivers/net/ethernet/broadcom/genet/bcmgenet.c | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/drivers/net/ethernet/broadcom/genet/bcmgenet.c b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
index 54f71b1e85fc..a1aa1278842e 100644
--- a/drivers/net/ethernet/broadcom/genet/bcmgenet.c
+++ b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
@@ -2018,10 +2018,10 @@ static int bcmgenet_tx_poll(struct napi_struct *napi, int budget)
 
 	spin_lock(&ring->lock);
 	work_done = __bcmgenet_tx_reclaim(ring->priv->dev, ring);
-	if (ring->free_bds > (MAX_SKB_FRAGS + 1)) {
-		txq = netdev_get_tx_queue(ring->priv->dev, ring->index);
+	txq = netdev_get_tx_queue(ring->priv->dev, ring->index);
+	if (netif_tx_queue_stopped(txq))
 		netif_tx_wake_queue(txq);
-	}
+
 	spin_unlock(&ring->lock);
 
 	if (work_done == 0) {
@@ -2224,9 +2224,6 @@ static netdev_tx_t bcmgenet_xmit(struct sk_buff *skb, struct net_device *dev)
 
 	netdev_tx_sent_queue(txq, GENET_CB(skb)->bytes_sent);
 
-	if (ring->free_bds <= (MAX_SKB_FRAGS + 1))
-		netif_tx_stop_queue(txq);
-
 	if (!netdev_xmit_more() || netif_xmit_stopped(txq))
 		/* Packets are ready, update producer index */
 		bcmgenet_tdma_ring_writel(priv, ring->index,
-- 
2.34.1


^ permalink raw reply related

* [PATCH net v2 3/4] net: bcmgenet: fix racing timeout handler
From: Justin Chen @ 2026-04-01  0:38 UTC (permalink / raw)
  To: netdev
  Cc: pabeni, kuba, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb,
	Justin Chen
In-Reply-To: <20260401003840.3112454-1-justin.chen@broadcom.com>

The bcmgenet_timeout handler tries to take down all tx queues when
a single queue times out. This is over zealous and causes many race
conditions with queues that are still chugging along. Instead lets
only restart the timed out queue.

Fixes: 13ea657806cf ("net: bcmgenet: improve TX timeout")
Signed-off-by: Justin Chen <justin.chen@broadcom.com>
Reviewed-by: Florian Fainelli <florian.fainelli@broadcom.com>
Reviewed-by: Nicolai Buchwitz <nb@tipi-net.de>
Tested-by: Nicolai Buchwitz <nb@tipi-net.de>
---
 .../net/ethernet/broadcom/genet/bcmgenet.c    | 22 ++++++++-----------
 1 file changed, 9 insertions(+), 13 deletions(-)

diff --git a/drivers/net/ethernet/broadcom/genet/bcmgenet.c b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
index e89126a0c20e..54f71b1e85fc 100644
--- a/drivers/net/ethernet/broadcom/genet/bcmgenet.c
+++ b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
@@ -3477,27 +3477,23 @@ static void bcmgenet_dump_tx_queue(struct bcmgenet_tx_ring *ring)
 static void bcmgenet_timeout(struct net_device *dev, unsigned int txqueue)
 {
 	struct bcmgenet_priv *priv = netdev_priv(dev);
-	u32 int1_enable = 0;
-	unsigned int q;
+	struct bcmgenet_tx_ring *ring = &priv->tx_rings[txqueue];
+	struct netdev_queue *txq = netdev_get_tx_queue(dev, txqueue);
 
 	netif_dbg(priv, tx_err, dev, "bcmgenet_timeout\n");
 
-	for (q = 0; q <= priv->hw_params->tx_queues; q++)
-		bcmgenet_dump_tx_queue(&priv->tx_rings[q]);
-
-	bcmgenet_tx_reclaim_all(dev);
+	bcmgenet_dump_tx_queue(ring);
 
-	for (q = 0; q <= priv->hw_params->tx_queues; q++)
-		int1_enable |= (1 << q);
+	bcmgenet_tx_reclaim(dev, ring, true);
 
-	/* Re-enable TX interrupts if disabled */
-	bcmgenet_intrl2_1_writel(priv, int1_enable, INTRL2_CPU_MASK_CLEAR);
+	/* Re-enable the TX interrupt for this ring */
+	bcmgenet_intrl2_1_writel(priv, 1 << txqueue, INTRL2_CPU_MASK_CLEAR);
 
-	netif_trans_update(dev);
+	txq_trans_cond_update(txq);
 
-	BCMGENET_STATS64_INC((&priv->tx_rings[txqueue].stats64), errors);
+	BCMGENET_STATS64_INC((&ring->stats64), errors);
 
-	netif_tx_wake_all_queues(dev);
+	netif_tx_wake_queue(txq);
 }
 
 #define MAX_MDF_FILTER	17
-- 
2.34.1


^ permalink raw reply related

* [PATCH net v2 2/4] net: bcmgenet: fix leaking free_bds
From: Justin Chen @ 2026-04-01  0:38 UTC (permalink / raw)
  To: netdev
  Cc: pabeni, kuba, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb,
	Justin Chen
In-Reply-To: <20260401003840.3112454-1-justin.chen@broadcom.com>

While reclaiming the tx queue we fast forward the write pointer to
drop any data in flight. These dropped frames are not added back
to the pool of free bds. We also need to tell the netdev that we
are dropping said data.

Fixes: f1bacae8b655 ("net: bcmgenet: support reclaiming unsent Tx packets")
Signed-off-by: Justin Chen <justin.chen@broadcom.com>
Reviewed-by: Florian Fainelli <florian.fainelli@broadcom.com>
Reviewed-by: Nicolai Buchwitz <nb@tipi-net.de>
Tested-by: Nicolai Buchwitz <nb@tipi-net.de>
---
 drivers/net/ethernet/broadcom/genet/bcmgenet.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/net/ethernet/broadcom/genet/bcmgenet.c b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
index 0f6e4baba25b..e89126a0c20e 100644
--- a/drivers/net/ethernet/broadcom/genet/bcmgenet.c
+++ b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
@@ -1985,6 +1985,7 @@ static unsigned int bcmgenet_tx_reclaim(struct net_device *dev,
 		drop = (ring->prod_index - ring->c_index) & DMA_C_INDEX_MASK;
 		released += drop;
 		ring->prod_index = ring->c_index & DMA_C_INDEX_MASK;
+		ring->free_bds += drop;
 		while (drop--) {
 			cb_ptr = bcmgenet_put_txcb(priv, ring);
 			skb = cb_ptr->skb;
@@ -1996,6 +1997,7 @@ static unsigned int bcmgenet_tx_reclaim(struct net_device *dev,
 		}
 		if (skb)
 			dev_consume_skb_any(skb);
+		netdev_tx_reset_queue(netdev_get_tx_queue(dev, ring->index));
 		bcmgenet_tdma_ring_writel(priv, ring->index,
 					  ring->prod_index, TDMA_PROD_INDEX);
 		wr_ptr = ring->write_ptr * WORDS_PER_BD(priv);
-- 
2.34.1


^ permalink raw reply related

* [PATCH net v2 1/4] net: bcmgenet: fix off-by-one in bcmgenet_put_txcb
From: Justin Chen @ 2026-04-01  0:38 UTC (permalink / raw)
  To: netdev
  Cc: pabeni, kuba, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb,
	Justin Chen
In-Reply-To: <20260401003840.3112454-1-justin.chen@broadcom.com>

The write_ptr points to the next open tx_cb. We want to return the
tx_cb that gets rewinded, so we must rewind the pointer first then
return the tx_cb that it points to. That way the txcb can be correctly
cleaned up.

Fixes: 876dbadd53a7 ("net: bcmgenet: Fix unmapping of fragments in bcmgenet_xmit()")
Signed-off-by: Justin Chen <justin.chen@broadcom.com>
---
 drivers/net/ethernet/broadcom/genet/bcmgenet.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/drivers/net/ethernet/broadcom/genet/bcmgenet.c b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
index 482a31e7b72b..0f6e4baba25b 100644
--- a/drivers/net/ethernet/broadcom/genet/bcmgenet.c
+++ b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
@@ -1819,15 +1819,15 @@ static struct enet_cb *bcmgenet_put_txcb(struct bcmgenet_priv *priv,
 {
 	struct enet_cb *tx_cb_ptr;
 
-	tx_cb_ptr = ring->cbs;
-	tx_cb_ptr += ring->write_ptr - ring->cb_ptr;
-
 	/* Rewinding local write pointer */
 	if (ring->write_ptr == ring->cb_ptr)
 		ring->write_ptr = ring->end_ptr;
 	else
 		ring->write_ptr--;
 
+	tx_cb_ptr = ring->cbs;
+	tx_cb_ptr += ring->write_ptr - ring->cb_ptr;
+
 	return tx_cb_ptr;
 }
 
-- 
2.34.1


^ permalink raw reply related

* [PATCH net v2 0/4] fix queue lock up and reduce timeouts
From: Justin Chen @ 2026-04-01  0:38 UTC (permalink / raw)
  To: netdev
  Cc: pabeni, kuba, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb,
	Justin Chen

We have been seeing reports of logs like this.
# [   39.713199] bcmgenet 1001300000.ethernet eth0: NETDEV WATCHDOG: CPU: 0: transmit queue 2 timed out 7991 ms
[   41.761198] bcmgenet 1001300000.ethernet eth0: NETDEV WATCHDOG: CPU: 0: transmit queue 2 timed out 10039 ms
[   43.745198] bcmgenet 1001300000.ethernet eth0: NETDEV WATCHDOG: CPU: 0: transmit queue 2 timed out 12023 ms
[   45.729198] bcmgenet 1001300000.ethernet eth0: NETDEV WATCHDOG: CPU: 0: transmit queue 2 timed out 14007 ms

We have two issues. The persistent queue timeouts and the eventual lock up of the entire transmit.

This is due to two issues.
- We have a small queue that is prone to timeouts. We optimize this by relaxing the tx queue stop conditions.
- We have a racing and leaking timeout handler that is also fixed.

v2
- These patches stayed the same.
  net: bcmgenet: fix off-by-one in bcmgenet_put_txcb
  net: bcmgenet: fix leaking free_bds
- New patches that fix a few other slightly related issues.
  net: bcmgenet: fix off-by-one in bcmgenet_put_txcb
  net: bcmgenet: relax the xmit ring full case

Justin Chen (4):
  net: bcmgenet: fix off-by-one in bcmgenet_put_txcb
  net: bcmgenet: fix leaking free_bds
  net: bcmgenet: fix racing timeout handler
  net: bcmgenet: relax the xmit ring full case

 .../net/ethernet/broadcom/genet/bcmgenet.c    | 39 ++++++++-----------
 1 file changed, 17 insertions(+), 22 deletions(-)

-- 
2.34.1


^ permalink raw reply

* Re: [PATCH net-next V9 02/14] devlink: Add helpers to lock nested-in instances
From: Jacob Keller @ 2026-03-31 23:55 UTC (permalink / raw)
  To: Cosmin Ratiu, Tariq Toukan, kuba@kernel.org
  Cc: allison.henderson@oracle.com, Moshe Shemesh, jiri@resnulli.us,
	davem@davemloft.net, daniel.zahka@gmail.com,
	donald.hunter@gmail.com, netdev@vger.kernel.org,
	matttbe@kernel.org, pabeni@redhat.com, horms@kernel.org,
	Parav Pandit, corbet@lwn.net, kees@kernel.org, willemb@google.com,
	Dragos Tatulea, razor@blackwall.org, Adithya Jayachandran,
	Dan Jurgens, leon@kernel.org, vadim.fedorenko@linux.dev,
	linux-kernel@vger.kernel.org, Saeed Mahameed, shuah@kernel.org,
	andrew+netdev@lunn.ch, Mark Bloch, Jiri Pirko, Carolina Jubran,
	Shahar Shitrit, daniel@iogearbox.net, minhquangbui99@gmail.com,
	Nimrod Oren, dw@davidwei.uk, skhan@linuxfoundation.org,
	Petr Machata, edumazet@google.com, antonio@openvpn.net,
	mst@redhat.com, linux-kselftest@vger.kernel.org,
	linux-rdma@vger.kernel.org, Shay Drori, sdf@fomichev.me,
	chuck.lever@oracle.com, Gal Pressman, joe@dama.to,
	linux-doc@vger.kernel.org
In-Reply-To: <ff5b2ec46d6cb639872318bdde429c46cac77f5b.camel@nvidia.com>

On 3/31/2026 5:20 AM, Cosmin Ratiu wrote:
> On Mon, 2026-03-30 at 19:08 -0700, Jakub Kicinski wrote:
>> This is an AI-generated review of your patch. The human sending this
>> email has considered the AI review valid, or at least plausible.
>> ---
>> devlink: Add helpers to lock nested-in instances
>>
>> Upcoming code will need to obtain a reference to locked nested-in
>> devlink instances. Add helpers to lock, obtain an already locked
>> reference and unlock/unref the nested-in instance.
>>
>>> diff --git a/net/devlink/core.c b/net/devlink/core.c
>>> index eeb6a71f5f56e..db11248df7120 100644
>>> --- a/net/devlink/core.c
>>> +++ b/net/devlink/core.c
>>
>> [ ... ]
>>
>>> +/* Returns the nested in devlink object and validates its lock is
>>> held. */
>>> +struct devlink *devlink_nested_in_get_locked(struct devlink_rel
>>> *rel)
>>> +{
>>> +	struct devlink *devlink;
>>> +	unsigned long index;
>>> +
>>> +	if (!rel)
>>> +		return NULL;
>>> +	index = rel->nested_in.devlink_index;
>>> +	devlink = xa_find(&devlinks, &index, index,
>>> DEVLINK_REGISTERED);
>>> +	if (devlink)
>>> +		devl_assert_locked(devlink);
>>
>> Can this dereference a freed pointer if the lock is not actually
>> held?
>>
>> xa_find() acquires and releases the RCU read lock internally. When it
>> returns,
>> the devlink pointer is no longer protected by RCU or a reference
>> count.
>>
>> If a caller invokes this helper without holding the devlink lock
>> (which is the
>> exact scenario the assertion is designed to detect), another thread
>> could
>> concurrently unregister and free the devlink.
>>
>> If the RCU grace period expires before the assertion executes,
>> devl_assert_locked() would dereference freed memory. Should the
>> lookup and
>> assertion be enclosed within an explicit rcu_read_lock() block to
>> safely
>> assert the lock?
> 
> This function is meant to be invoked by callers already holding all
> locks from a given instance up the nesting tree. Calling it outside
> this context could indeed lead to a race as described, where another
> entity unregisters a devlink about-to-be-asserted on.
> 

Hmm. I'm struggling to follow this. If you already expect the parent to
hold the nested devlink's lock, it must have a pointer to this devlink
instance. In that case, why would you even need
devlink_nested_in_get_locked in the first place?

> All current callers hold the locks, but to make it more robust, I will
> add the rcu_read_lock/unlock section around the find+assert.
> 

You should at least do that. It is the correct pattern when dealing with
reference counting data structures from the xarray. That's already how
devlinks_xa_get() is implemented.

>>
>>> +	return devlink;
>>> +}
>>> +
>>> +void devlink_nested_in_put_unlock(struct devlink_rel *rel)
>>> +{
>>> +	struct devlink *devlink =
>>> devlink_nested_in_get_locked(rel);
>>
>> Could this corrupt the mutex state and cause a refcount underflow?
>>
>> This helper takes a devlink_rel pointer instead of the devlink
>> pointer
>> acquired by devlink_nested_in_get_lock(), and performs a secondary
>> global
>> lookup to find the devlink.
>>
>> If a caller mistakenly calls this in an error cleanup path where they
>> did not
>> actually acquire the lock, the global xa_find() will still locate the
>> registered devlink. This would execute devl_unlock() and
>> devlink_put() on a
>> devlink the current thread does not own.
>>

If the caller already held the lock, why is devlink_nested_in_put_unlock
calling the devl_unlock instead of the caller anyways? That seems
confusing. Wouldn't the normal pattern be to
devlink_nested_in_get_lock()? Oh, that is a separate function. Ok I see.

>> Would it be safer for unlock/put helpers to take the exact pointer
>> returned by
>> the lock/get helper to ensure safe resource cleanup?
> 
> 2 issues here:
> 1) Mistakenly calling this without having acquired the lock. This is
> akin to saying mutex_unlock is dangerous if the lock isn't held.
> Technically true, but moot.
> 2) The rel argument: It is intentional, so that all 3 functions are
> symmetrical.
> 

IMO it would make more sense for the put version to be a put on the
returned devlink pointer. I guess its not symmetrical, but it removes
the need to perform the second lookup and it makes it easier to reason
about the pointer you're releasing being the same one.

Having put take different arguments from get is the usual pattern for
such a behavior.

Also devlink_nested_in_get_locked() doesn't increase the ref count so it
is sort of "relying" on the caller already having a reference to it,
which makes me think its not very useful. The only valid way to call
this function as it exists now safely is to already hold a reference to
the object, which also already requires you to have a valid pointer
making me wonder why you'd ever need to call it in the first place.

The only example you have is to make devlink_nested_in_put_unlock() take
a devlink_rel pointer as its argument instead of just calling it on the
pointer returned by devlink_nested_in_get_lock().

This implementation seems confusing and likely to lead to errors.

Thanks,
Jake

^ permalink raw reply

* Re: [PATCH v2] net: ns83820: fix DMA mapping error handling in hard_start_xmit
From: Joe Damato @ 2026-03-31 23:49 UTC (permalink / raw)
  To: Wang Jun
  Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, netdev, linux-kernel, gszhai, 25125332, 25125283,
	23120469
In-Reply-To: <tencent_EB7280CE1394ABF5035CDA078FC60A481906@qq.com>

On Tue, Mar 31, 2026 at 08:21:41PM +0800, Wang Jun wrote:
> Hi Paolo Abeni,
> 
> This is v2 of the DMA mapping error handling fix for ns83820. Changes since v1:
> 
> - Added queue restart check in error path to avoid potential TX queue stall
>   (as pointed out by the AI review)
> - Adjusted variable declarations to follow reverse christmas tree order
> - Switched from dma_unmap_single to dma_unmap_page for fragments

Usually you'd put the changelog below. Take a look at how this recent patch
does it:

https://lore.kernel.org/netdev/20260331123858.1912449-2-charles.perry@microchip.com/

> diff --git a/drivers/net/ethernet/natsemi/ns83820.c b/drivers/net/ethernet/natsemi/ns83820.c
> index cdbf82affa7b..f8d037db4ffb 100644
> --- a/drivers/net/ethernet/natsemi/ns83820.c
> +++ b/drivers/net/ethernet/natsemi/ns83820.c
> @@ -1051,6 +1051,12 @@ static netdev_tx_t ns83820_hard_start_xmit(struct sk_buff *skb,
>  	int stopped = 0;
>  	int do_intr = 0;
>  	volatile __le32 *first_desc;
> +	int i;
> +	int frag_mapped_count = 0;
> +	unsigned int main_len = 0;
> +	unsigned int frag_dma_len[MAX_SKB_FRAGS];
> +	dma_addr_t main_buf = 0;
> +	dma_addr_t frag_dma_addr[MAX_SKB_FRAGS];

You mentioned it was fixed to reverse christmas tree in the changelog above,
but doesn't seem like it in the patch?

^ permalink raw reply

* Re: [PATCH v9 net-next 2/5] psp: add new netlink cmd for dev-assoc and dev-disassoc
From: kernel test robot @ 2026-03-31 23:43 UTC (permalink / raw)
  To: Wei Wang, netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn,
	David Wei, Andrew Lunn, David S . Miller, Eric Dumazet,
	Simon Horman
  Cc: oe-kbuild-all, Wei Wang
In-Reply-To: <20260330223143.2394706-3-weibunny.kernel@gmail.com>

Hi Wei,

kernel test robot noticed the following build warnings:

[auto build test WARNING on net-next/main]

url:    https://github.com/intel-lab-lkp/linux/commits/Wei-Wang/psp-add-admin-non-admin-version-of-psp_device_get_locked/20260331-065558
base:   net-next/main
patch link:    https://lore.kernel.org/r/20260330223143.2394706-3-weibunny.kernel%40gmail.com
patch subject: [PATCH v9 net-next 2/5] psp: add new netlink cmd for dev-assoc and dev-disassoc
config: sh-randconfig-r113-20260401 (https://download.01.org/0day-ci/archive/20260401/202604010708.Yck6IfPj-lkp@intel.com/config)
compiler: sh4-linux-gcc (GCC) 15.2.0
sparse: v0.6.5-rc1
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260401/202604010708.Yck6IfPj-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202604010708.Yck6IfPj-lkp@intel.com/

sparse warnings: (new ones prefixed by >>)
>> net/psp/psp_nl.c:494:13: sparse: sparse: incorrect type in initializer (different address spaces) @@     expected struct psp_dev [noderef] __rcu *_n_ @@     got struct psp_dev *psd @@
   net/psp/psp_nl.c:494:13: sparse:     expected struct psp_dev [noderef] __rcu *_n_
   net/psp/psp_nl.c:494:13: sparse:     got struct psp_dev *psd

vim +494 net/psp/psp_nl.c

   449	
   450	int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info)
   451	{
   452		struct psp_dev *psd = info->user_ptr[0];
   453		struct psp_assoc_dev *psp_assoc_dev;
   454		struct net_device *assoc_dev;
   455		u32 assoc_ifindex;
   456		struct sk_buff *rsp;
   457		struct net *net;
   458		int nsid;
   459	
   460		if (GENL_REQ_ATTR_CHECK(info, PSP_A_DEV_IFINDEX))
   461			return -EINVAL;
   462	
   463		if (info->attrs[PSP_A_DEV_NSID]) {
   464			nsid = nla_get_s32(info->attrs[PSP_A_DEV_NSID]);
   465	
   466			net = get_net_ns_by_id(genl_info_net(info), nsid);
   467			if (!net) {
   468				NL_SET_BAD_ATTR(info->extack,
   469						info->attrs[PSP_A_DEV_NSID]);
   470				return -EINVAL;
   471			}
   472		} else {
   473			net = get_net(genl_info_net(info));
   474		}
   475	
   476		psp_assoc_dev = kzalloc(sizeof(*psp_assoc_dev), GFP_KERNEL);
   477		if (!psp_assoc_dev) {
   478			put_net(net);
   479			return -ENOMEM;
   480		}
   481	
   482		assoc_ifindex = nla_get_u32(info->attrs[PSP_A_DEV_IFINDEX]);
   483		assoc_dev = netdev_get_by_index(net, assoc_ifindex,
   484						&psp_assoc_dev->dev_tracker,
   485						GFP_KERNEL);
   486		if (!assoc_dev) {
   487			put_net(net);
   488			kfree(psp_assoc_dev);
   489			NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_DEV_IFINDEX]);
   490			return -ENODEV;
   491		}
   492	
   493		/* Check if device is already associated with a PSP device */
 > 494		if (cmpxchg(&assoc_dev->psp_dev, NULL, psd)) {
   495			NL_SET_ERR_MSG(info->extack,
   496				       "Device already associated with a PSP device");
   497			netdev_put(assoc_dev, &psp_assoc_dev->dev_tracker);
   498			put_net(net);
   499			kfree(psp_assoc_dev);
   500			return -EBUSY;
   501		}
   502	
   503		psp_assoc_dev->assoc_dev = assoc_dev;
   504		rsp = psp_nl_reply_new(info);
   505		if (!rsp) {
   506			rcu_assign_pointer(assoc_dev->psp_dev, NULL);
   507			netdev_put(assoc_dev, &psp_assoc_dev->dev_tracker);
   508			put_net(net);
   509			kfree(psp_assoc_dev);
   510			return -ENOMEM;
   511		}
   512	
   513		list_add_tail(&psp_assoc_dev->dev_list, &psd->assoc_dev_list);
   514	
   515		put_net(net);
   516	
   517		psp_nl_notify_dev(psd, PSP_CMD_DEV_CHANGE_NTF);
   518	
   519		return psp_nl_reply_send(rsp, info);
   520	}
   521	

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* Re: [PATCH net-next v3 2/2] net: mdio: add a driver for PIC64-HPSC/HX MDIO controller
From: Andrew Lunn @ 2026-03-31 23:43 UTC (permalink / raw)
  To: Charles Perry
  Cc: Russell King (Oracle), netdev, Maxime Chevallier, Heiner Kallweit,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	linux-kernel
In-Reply-To: <acvdkBF4ZjZaCHwd@bby-cbu-swbuild03.eng.microchip.com>

> > > Andrew?
> > 
> > The general pattern is to not wait on write.
> 
> Ok, then it's status quo for this.
> 
> I'll send a v4 later this week for the other return issue.

Please consider adding a one line patch to phy_disable_interrupts to
read register MII_PHYSID1 and throw away the result.

     Andrew

^ permalink raw reply

* Re: [PATCH] net: alteon: Add missing DMA mapping error checks in ace_start_xmit
From: Joe Damato @ 2026-03-31 23:43 UTC (permalink / raw)
  To: Wang Jun
  Cc: Jes Sorensen, Andrew Lunn, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-acenic, netdev, linux-kernel,
	gszhai, 25125332, 25125283, 23120469, stable
In-Reply-To: <tencent_FAB2A00E105488F503DCC787B8060F881E06@qq.com>

On Tue, Mar 31, 2026 at 09:48:41AM +0800, Wang Jun wrote:
> The ace_start_xmit function does not check the return value of
> dma_map_page (via ace_map_tx_skb) and skb_frag_dma_map when building
> transmit descriptors. If mapping fails, an invalid DMA address is
> written to the descriptor, which may cause hardware to access
> illegal memory, leading to system instability or crashes.
> 
> Add proper dma_mapping_error() checks for all mapping calls. When
> mapping fails, free the skb, increment the dropped packet counter,
> and return NETDEV_TX_OK.
> 
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")

Is this fixing a bug you've seen in the wild? If not, I'd probably drop the
fixes tag and send this to net-next instead.

> Cc: stable@vger.kernel.org
> Signed-off-by: Wang Jun <1742789905@qq.com>
> ---
>  drivers/net/ethernet/alteon/acenic.c | 15 +++++++++++++++
>  1 file changed, 15 insertions(+)
> 
> diff --git a/drivers/net/ethernet/alteon/acenic.c b/drivers/net/ethernet/alteon/acenic.c
> index 455ee8930824..acabede53663 100644
> --- a/drivers/net/ethernet/alteon/acenic.c
> +++ b/drivers/net/ethernet/alteon/acenic.c
> @@ -2417,6 +2417,11 @@ static netdev_tx_t ace_start_xmit(struct sk_buff *skb,
>  		u32 vlan_tag = 0;
>  
>  		mapping = ace_map_tx_skb(ap, skb, skb, idx);
> +		if (dma_mapping_error(&ap->pdev->dev, mapping)) {
> +			dev_kfree_skb(skb);
> +			dev->stats.tx_dropped++;
> +			return NETDEV_TX_OK;
> +		}
>  		flagsize = (skb->len << 16) | (BD_FLG_END);
>  		if (skb->ip_summed == CHECKSUM_PARTIAL)
>  			flagsize |= BD_FLG_TCP_UDP_SUM;
> @@ -2438,6 +2443,11 @@ static netdev_tx_t ace_start_xmit(struct sk_buff *skb,
>  		int i;
>  
>  		mapping = ace_map_tx_skb(ap, skb, NULL, idx);
> +		if (dma_mapping_error(&ap->pdev->dev, mapping)) {
> +			dev_kfree_skb(skb);
> +			dev->stats.tx_dropped++;
> +			return NETDEV_TX_OK;
> +		}


I am not sure about this. The function ace_map_tx_skb seems to modify a
tx_ring_info entry, possibly writing invalid state to it if the dma_map_page
fails?

Maybe a better fix would be to refactor ace_map_tx_skb and do the
dma_mapping_error check there and change the return type of the function and
the code flow?

Then you wouldn't need to duplicate the error path handling code.

^ permalink raw reply

* Re: [PATCH bpf v3 5/5] bpf, sockmap: Adapt for af_unix-specific lock
From: Kuniyuki Iwashima @ 2026-03-31 23:18 UTC (permalink / raw)
  To: Michal Luczaj
  Cc: Martin KaFai Lau, Jiayuan Chen, John Fastabend, Jakub Sitnicki,
	Eric Dumazet, Paolo Abeni, Willem de Bruijn, David S. Miller,
	Jakub Kicinski, Simon Horman, Yonghong Song, Andrii Nakryiko,
	Alexei Starovoitov, Daniel Borkmann, Eduard Zingerman, Song Liu,
	Yonghong Song, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
	Shuah Khan, Cong Wang, netdev, bpf, linux-kernel, linux-kselftest
In-Reply-To: <48bad987-a130-4fb0-b4de-e39deac80ab9@rbox.co>

On Tue, Mar 31, 2026 at 3:44 PM Michal Luczaj <mhal@rbox.co> wrote:
>
> On 3/31/26 01:27, Kuniyuki Iwashima wrote:
> > On Mon, Mar 30, 2026 at 4:04 PM Michal Luczaj <mhal@rbox.co> wrote:
> >> On 3/26/26 07:26, Martin KaFai Lau wrote:
> >>> On 3/15/26 4:58 PM, Michal Luczaj wrote:
> >>>>> Beside, from looking at the may_update_sockmap(), I don't know if it is
> >>>>> even doable (or useful) to bpf_map_update_elem(unix_sk) in
> >>>>> tc/flow_dissector/xdp. One possible path is the SOCK_FILTER when looking
> >>>>> at unix_dgram_sendmsg() => sk_filter(). It was not the original use case
> >>>>> when the bpf_map_update_elem(sockmap) support was added iirc.
> >>>>
> >>>> What about a situation when unix_sk is stored in a sockmap, then tc prog
> >>>> looks it up and invokes bpf_map_update_elem(unix_sk)? I'm not sure it's
> >>>> useful, but seems doable.
> >>>
> >>> [ Sorry for the late reply ]
> >>>
> >>> It is a bummer that the bpf_map_update_elem(unix_sk) path is possible
> >>> from tc :(
> >>>
> >>> Then unix_state_lock() in its current form cannot be safely acquired in
> >>> sock_map_update_elem(). It is currently a spin_lock() instead of
> >>> spin_lock_bh().
> >>
> >> Is there a specific deadlock you have in your mind?
> >
> > lockdep would complain if we used the same lock from
> > different contexts.
> >
> > e.g.)
> > Process context holding unix_state_lock() with the normal spin_lock()
> > -> BH interrupt
> > -> tc prog trying to hold the same lock with spin_lock(). (_bh())
> > -> deadlock
>
> OK, I get it, thanks.
>
> So here's one more idea: the null-ptr-deref issue is connect() racing
> against sock_map_update_elem_*SYS*() coming from user-space, not the
> can-be-called-from-BH sock_map_update_elem() variant. So can't we assume
> that for any sock_map_update_elem(unix_sk) invoked by a tc prog, unix_sk
> will always be "stable", i.e. in a state that cannot lead to that
> null-ptr-deref?
>
> IOW, if for a tc prog the only way to get hold of unix_sk is look it up in
> a sockmap, then (by the fact that unix_sk _is_ in the sockmap) unix_sk will
> be already safe to use by sock_map_update_elem() without taking the af_unix
> state lock.
>
> Long story short: take the unix state lock in sock_map_update_elem_sys(),
> don't bother in sock_map_update_elem()?

but it will prevents bpf iter from taking unix_state_lock().

I don't see a good reason to introduce a new locking rule by unnecessarily
wrapping the entire sockmap update with unix_state_lock() even though
the root bug can be avoided by a simple null check in a leaf function.


>
> >> ...
> >> And sock_{map,hash}_seq_show() (being a part of bpf iter machinery) needs
> >> to take lock_sock() just as well? Would that require a special-casing
> >> (unix_state_lock()) for af_unix?
> >
> > Right, lock_sock() + unix_state_lock() + SOCK_DEAD check
> > should be best.
>
> OK, got it.
>

^ permalink raw reply

* Re: [PATCH net 2/2] net: bcmgenet: fix racing timeout handler
From: Justin Chen @ 2026-03-31 23:15 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: netdev, pabeni, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb
In-Reply-To: <20260327211331.78476a99@kernel.org>



On 3/27/26 9:13 PM, Jakub Kicinski wrote:
> On Thu, 26 Mar 2026 11:45:29 -0700 justin.chen@broadcom.com wrote:
>> The bcmgenet_timeout handler tries to take down all tx queues when
>> a single queue times out. This is over zealous and causes many race
>> conditions with queues that are still chugging along. Instead lets
>> only restart the timed out queue.
> 
> FWIW AI seems to suggest we should also stop NAPI and the DMA in this
> case, just to make sure that the queue in question doesn't suddenly
> wake up either. Which seems fair but probably as a follow up and only
> if not too hard in itself..

Yea this crossed my mind. I was thinking we might need to grab the ring 
lock here in case we race on the napi tx relcaim. But in my testing, I 
wasn't able to reproduce the lock up with these changes as is. But might 
be worth throwing a lock in there anyways. I don't think we need to halt 
the DMA, since we clean up everything, worst outcome is a packet gets 
sent out when we tag it as being dropped.

Thanks,
Justin

^ permalink raw reply

* Re: [PATCH net 1/2] net: bcmgenet: fix leaking free_bds
From: Justin Chen @ 2026-03-31 23:11 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: netdev, pabeni, edumazet, davem, andrew+netdev,
	bcm-kernel-feedback-list, florian.fainelli, opendmb, nb
In-Reply-To: <20260327211127.7e9317b3@kernel.org>



On 3/27/26 9:11 PM, Jakub Kicinski wrote:
> On Thu, 26 Mar 2026 11:45:28 -0700 justin.chen@broadcom.com wrote:
>> From: Justin Chen <justin.chen@broadcom.com>
>>
>> While reclaiming the tx queue we fast forward the write pointer to
>> drop any data in flight. These dropped frames are not added back
>> to the pool of free bds. We also need to tell the netdev that we
>> are dropping said data.
> 
>> diff --git a/drivers/net/ethernet/broadcom/genet/bcmgenet.c b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
>> index 482a31e7b72b..3e1fc3bb8297 100644
>> --- a/drivers/net/ethernet/broadcom/genet/bcmgenet.c
>> +++ b/drivers/net/ethernet/broadcom/genet/bcmgenet.c
>> @@ -1985,6 +1985,7 @@ static unsigned int bcmgenet_tx_reclaim(struct net_device *dev,
>>   		drop = (ring->prod_index - ring->c_index) & DMA_C_INDEX_MASK;
>>   		released += drop;
>>   		ring->prod_index = ring->c_index & DMA_C_INDEX_MASK;
>> +		ring->free_bds += drop;
>>   		while (drop--) {
>>   			cb_ptr = bcmgenet_put_txcb(priv, ring);
>>   			skb = cb_ptr->skb;
>> @@ -1996,6 +1997,7 @@ static unsigned int bcmgenet_tx_reclaim(struct net_device *dev,
>>   		}
>>   		if (skb)
>>   			dev_consume_skb_any(skb);
>> +		netdev_tx_reset_queue(netdev_get_tx_queue(dev, ring->index));
>>   		bcmgenet_tdma_ring_writel(priv, ring->index,
>>   					  ring->prod_index, TDMA_PROD_INDEX);
>>   		wr_ptr = ring->write_ptr * WORDS_PER_BD(priv);
> 
> AI says you may be off by one here?
> 
>    Does this loop miss the oldest dropped descriptor and leak its SKB and
>    DMA mapping?
> 
>    Since bcmgenet_get_txcb() increments write_ptr after a transmission,
>    write_ptr always points to the next available empty slot.
> 
>    When this loops backwards drop times, the first iteration retrieves that
>    empty descriptor (where cb->skb is NULL), and the loop will finish before
>    reaching the oldest uncompleted descriptor at c_index.
> 
>    When write_ptr is rolled back to c_index, won't subsequent transmissions
>    overwrite the descriptor at c_index and permanently leak the unmapped DMA
>    buffer and SKB memory?

Ack. Looks like this is another bug. Yes, it causes an off-by-one in 
this case, but also in the xmit error path case. So I think this 
warrants another commit. Will submit v3 with this as another bug fix.

Thanks,
Justin

^ permalink raw reply

* Re: [PATCH net-next v6 04/11] net: ngbe: implement libwx reset ops
From: Jacob Keller @ 2026-03-31 23:05 UTC (permalink / raw)
  To: Jiawen Wu, 'Jakub Kicinski'
  Cc: netdev, mengyuanlou, andrew+netdev, davem, edumazet, pabeni,
	richardcochran, linux, horms, michal.swiatkowski, kees, joe,
	larysa.zaremba, abdun.nihaal, leitao
In-Reply-To: <073601dcc0de$a87b2e30$f9718a90$@trustnetic.com>

On 3/31/2026 12:19 AM, Jiawen Wu wrote:
>>> +static void ngbe_reinit_locked(struct wx *wx)
>>> +{
>>> +	int err = 0;
>>> +
>>> +	netif_trans_update(wx->netdev);
>>> +
>>> +	err = wx_set_state_reset(wx);
>>> +	if (err) {
>>> +		wx_err(wx, "wait device reset timeout\n");
>>> +		return;
>>> +	}
>>> +
>>> +	ngbe_down(wx);
>>> +	ngbe_up(wx);
>>> +
>>> +	clear_bit(WX_STATE_RESETTING, wx->state);
>>> +}
>>
>> Is it possible to use a standard kernel mutex here instead of a bit flag for
>> synchronization?
>>
>> Looking at the underlying implementation of wx_set_state_reset(), it relies
>> on an open-coded polling loop:
>>
>> static inline int wx_set_state_reset(struct wx *wx)
>> {
>>         u8 timeout = 50;
>>
>>         while (test_and_set_bit(WX_STATE_RESETTING, wx->state)) {
>>                 timeout--;
>>                 if (!timeout)
>>                         return -EBUSY;
>>
>>                 usleep_range(1000, 2000);
>>         }
>>
>>         return 0;
>> }
>>
>> Using a bit flag and a sleep-polling loop to guard a teardown and bringup
>> section acts as an ad-hoc lock. This approach bypasses standard kernel
>> synchronization guarantees and prevents lockdep from analyzing the lock
>> ordering.
>>
>> Since this context is allowed to sleep, could this section be protected by
>> a standard mutex instead?
> > I think using a state flag here would be better. Because other code
paths
> (like watchdog) need to check if a reset is in process without taking a lock.
> 
> 


Couldn't you use an atomic and a mutex together?

mutex_lock(wx->reset_lock);
set_bit(WX_STATE_RESETTING, wx->state);
...
Mutex_unlock(wx->reset_lock);

Paths that only need to know a reset has already started can just
test_bit. Paths which need to wait on reset can acquire the lock.

There shouldn't be any issue doing this that doesn't already exist with
the test_and_set loop, because you can already have a path doing "if
(test_bit(resetting)) exit" flow would still potentially begin executing
even if a reset already acquired the lock, it could have ordered the
test path first regardless, so you already aren't really guaranteed that
a reset won't start until your operation is done unless you hold the lock.

Using a mutex also gives you lockdep checking and is generally more sane
to reason about.

Thanks,
Jake

^ permalink raw reply

* [net-next PATCH 10/10] net: dsa: tag_rtl8_4: set KEEP flag
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
	Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
  Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>

KEEP=1 is needed because we should respect the format of the packet as
the kernel sends it to us. Unless tx forward offloading is used, the
kernel is giving us the packet exactly as it should leave the specified
port on the wire. Until now this was not needed because the ports were
always functioning in a standalone mode in a VLAN-unaware way, so the
switch would not tag or untag frames anyway. But arguably it should have
been KEEP=1 all along.

Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
 net/dsa/tag_rtl8_4.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/net/dsa/tag_rtl8_4.c b/net/dsa/tag_rtl8_4.c
index b7ed39c5419f..852c6b88079a 100644
--- a/net/dsa/tag_rtl8_4.c
+++ b/net/dsa/tag_rtl8_4.c
@@ -99,6 +99,7 @@
 #define   RTL8_4_REASON_TRAP		80
 
 #define RTL8_4_LEARN_DIS		BIT(5)
+#define RTL8_4_KEEP			BIT(7)
 
 #define RTL8_4_TX			GENMASK(3, 0)
 #define RTL8_4_RX			GENMASK(10, 0)
@@ -114,8 +115,9 @@ static void rtl8_4_write_tag(struct sk_buff *skb, struct net_device *dev,
 	/* Set Protocol; zero REASON */
 	tag16[1] = htons(FIELD_PREP(RTL8_4_PROTOCOL, RTL8_4_PROTOCOL_RTL8365MB));
 
-	/* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX, KEEP; set LEARN_DIS */
-	tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1));
+	/* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX; set KEEP, LEARN_DIS */
+	tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1) |
+			 FIELD_PREP(RTL8_4_KEEP, 1));
 
 	/* Zero ALLOW; set RX (CPU->switch) forwarding port mask */
 	tag16[3] = htons(FIELD_PREP(RTL8_4_RX, dsa_xmit_port_mask(skb, dev)));

-- 
2.53.0


^ permalink raw reply related

* [net-next PATCH 09/10] net: dsa: realtek: rtl8365mb: add bridge port flags
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
	Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
  Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>

From: Alvin Šipraga <alsi@bang-olufsen.dk>

Add bridge port flags for:
- BR_LEARNING
- BR_FLOOD
- BR_MCAST_FLOOD
- BR_BCAST_FLOOD

Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
 drivers/net/dsa/realtek/rtl8365mb_main.c | 125 +++++++++++++++++++++++++++++++
 1 file changed, 125 insertions(+)

diff --git a/drivers/net/dsa/realtek/rtl8365mb_main.c b/drivers/net/dsa/realtek/rtl8365mb_main.c
index 0767b2704ad7..b26335c5b27e 100644
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -302,6 +302,21 @@
 #define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
 		(0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
 
+/* Unknown unicast DA flooding port mask */
+#define RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_REG		0x0890
+#define   RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_MASK		0x07FF
+
+/* Unknown multicast DA flooding port mask */
+#define RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_REG		0x0891
+#define   RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_MASK	0x07FF
+
+/* Broadcast flooding port mask */
+#define RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_REG		0x0892
+#define   RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_MASK	0x07FF
+
+#define RTL8365MB_SUPPORTED_BRIDGE_FLAGS \
+	    (BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD)
+
 /* Miscellaneous port configuration register, incl. VLAN egress mode */
 #define RTL8365MB_PORT_MISC_CFG_REG_BASE			0x000E
 #define RTL8365MB_PORT_MISC_CFG_REG(_p) \
@@ -1641,6 +1656,97 @@ static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
 			    enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
 }
 
+static int rtl8365mb_port_set_ucast_flood(struct realtek_priv *priv, int port,
+					  bool enable)
+{
+	/* Frames with unknown unicast DA will be flooded to a programmable
+	 * port mask that by default includes all ports. Add or remove
+	 * the specified port from this port mask accordingly.
+	 */
+	return regmap_update_bits(priv->map,
+				  RTL8365MB_UNKNOWN_UNICAST_FLOODING_PMASK_REG,
+				  BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_set_mcast_flood(struct realtek_priv *priv, int port,
+					  bool enable)
+{
+	return regmap_update_bits(priv->map,
+			RTL8365MB_UNKNOWN_MULTICAST_FLOODING_PMASK_REG,
+			BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_set_bcast_flood(struct realtek_priv *priv, int port,
+					  bool enable)
+{
+	return regmap_update_bits(priv->map,
+			RTL8365MB_UNKNOWN_BROADCAST_FLOODING_PMASK_REG,
+			BIT(port), enable ? BIT(port) : 0);
+}
+
+static int rtl8365mb_port_pre_bridge_flags(struct dsa_switch *ds, int port,
+					   struct switchdev_brport_flags flags,
+					   struct netlink_ext_ack *extack)
+{
+	struct realtek_priv *priv = ds->priv;
+
+	dev_dbg(priv->dev, "pre_bridge_flags port:%d flags:%lx supported:%lx",
+		port, flags.mask, RTL8365MB_SUPPORTED_BRIDGE_FLAGS);
+
+	if (flags.mask & ~RTL8365MB_SUPPORTED_BRIDGE_FLAGS)
+		return -EINVAL;
+
+	return 0;
+}
+
+static int rtl8365mb_port_bridge_flags(struct dsa_switch *ds, int port,
+				       struct switchdev_brport_flags flags,
+				       struct netlink_ext_ack *exack)
+{
+	struct realtek_priv *priv = ds->priv;
+	int ret;
+
+	dev_dbg(priv->dev, "port_bridge_flags port:%d flags:%lx supported:%lx",
+		port, flags.mask, RTL8365MB_SUPPORTED_BRIDGE_FLAGS);
+
+	if (flags.mask & BR_LEARNING) {
+		bool learning_en = !!(flags.val & BR_LEARNING);
+
+		ret = rtl8365mb_port_set_learning(priv, port, learning_en);
+		if (ret)
+			return ret;
+	}
+
+	if (flags.mask & BR_FLOOD) {
+		bool ucast_flood_en = !!(flags.val & BR_FLOOD);
+
+		ret = rtl8365mb_port_set_ucast_flood(priv, port,
+						     ucast_flood_en);
+		if (ret)
+			return ret;
+	}
+
+	if (flags.mask & BR_MCAST_FLOOD) {
+		bool mcast_flood_en = !!(flags.val & BR_MCAST_FLOOD);
+
+		ret = rtl8365mb_port_set_mcast_flood(priv, port,
+						     mcast_flood_en);
+		if (ret)
+			return ret;
+	}
+
+	if (flags.mask & BR_BCAST_FLOOD) {
+		bool bcast_flood_en = !!(flags.val & BR_BCAST_FLOOD);
+
+		ret = rtl8365mb_port_set_bcast_flood(priv, port,
+						     bcast_flood_en);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
 static int rtl8365mb_port_set_efid(struct realtek_priv *priv, int port,
 				   u32 efid)
 {
@@ -1691,6 +1797,8 @@ static int rtl8365mb_port_bridge_join(struct dsa_switch *ds, int port,
 	int ret;
 	int i;
 
+	dev_dbg(priv->dev, "bridge %d join port %d\n", port, bridge.num);
+
 	/* Add this port to the isolation group of every other port
 	 * offloading this bridge.
 	 */
@@ -1730,6 +1838,8 @@ static void rtl8365mb_port_bridge_leave(struct dsa_switch *ds, int port,
 	u32 mask = 0;
 	int i;
 
+	dev_dbg(priv->dev, "bridge %d leave port %d\n", port, bridge.num);
+
 	/* Remove this port from the isolation group of every other
 	 * port offloading this bridge.
 	 */
@@ -2547,6 +2657,19 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
 		if (ret)
 			goto out_teardown_irq;
 
+		/* Enable all types of flooding */
+		ret = rtl8365mb_port_set_ucast_flood(priv, i, true);
+		if (ret)
+			goto out_teardown_irq;
+
+		ret = rtl8365mb_port_set_mcast_flood(priv, i, true);
+		if (ret)
+			goto out_teardown_irq;
+
+		ret = rtl8365mb_port_set_bcast_flood(priv, i, true);
+		if (ret)
+			goto out_teardown_irq;
+
 		/* Set up per-port private data */
 		p->priv = priv;
 		p->index = i;
@@ -2686,6 +2809,8 @@ static const struct dsa_switch_ops rtl8365mb_switch_ops = {
 	.phylink_get_caps = rtl8365mb_phylink_get_caps,
 	.port_bridge_join = rtl8365mb_port_bridge_join,
 	.port_bridge_leave = rtl8365mb_port_bridge_leave,
+	.port_pre_bridge_flags = rtl8365mb_port_pre_bridge_flags,
+	.port_bridge_flags = rtl8365mb_port_bridge_flags,
 	.port_stp_state_set = rtl8365mb_port_stp_state_set,
 	.port_fast_age = rtl8365mb_port_fast_age,
 	.port_fdb_add = rtl8365mb_port_fdb_add,

-- 
2.53.0


^ permalink raw reply related

* [net-next PATCH 08/10] net: dsa: realtek: rtl8365mb: add FDB support
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
	Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
  Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>

From: Alvin Šipraga <alsi@bang-olufsen.dk>

Add support for forwarding database operations, including unicast and
multicast entry handling as well as fast aging support.

The driver implements Independent VLAN Learning (IVL) by keying the
forwarding database with the {VID, MAC, EFID} tuple. The Extended
Filtering ID (EFID) is 3 bits wide, providing 8 unique filtering
domains. Since EFID 0 is reserved for standalone ports where learning is
disabled, the hardware is limited to offloading a maximum of 7 bridges.

The driver implements only the subset of L2 table accessors needed to
support these DSA operations. Other hardware capabilities are left
untouched and can be added incrementally if needed.

Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
 drivers/net/dsa/realtek/Makefile         |   1 +
 drivers/net/dsa/realtek/rtl8365mb_l2.c   | 465 +++++++++++++++++++++++++++++++
 drivers/net/dsa/realtek/rtl8365mb_l2.h   |  59 ++++
 drivers/net/dsa/realtek/rtl8365mb_main.c | 197 ++++++++++++-
 4 files changed, 721 insertions(+), 1 deletion(-)

diff --git a/drivers/net/dsa/realtek/Makefile b/drivers/net/dsa/realtek/Makefile
index b7fc4e852fd8..6c329e046d0b 100644
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -19,3 +19,4 @@ obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
 rtl8365mb-objs := rtl8365mb_main.o \
 		  rtl8365mb_table.o \
 		  rtl8365mb_vlan.o \
+		  rtl8365mb_l2.o \
diff --git a/drivers/net/dsa/realtek/rtl8365mb_l2.c b/drivers/net/dsa/realtek/rtl8365mb_l2.c
new file mode 100644
index 000000000000..be5825f8fe7b
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_l2.c
@@ -0,0 +1,465 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Forwarding and multicast database interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#include <linux/etherdevice.h>
+
+#include "rtl8365mb_l2.h"
+#include "rtl8365mb_table.h"
+#include <linux/regmap.h>
+
+#define RTL8365MB_L2_ENTRY_SIZE			6
+
+#define RTL8365MB_L2_UC_D0_MAC5_MASK		GENMASK(7, 0)
+#define RTL8365MB_L2_UC_D0_MAC4_MASK		GENMASK(15, 8)
+#define RTL8365MB_L2_UC_D1_MAC3_MASK		GENMASK(7, 0)
+#define RTL8365MB_L2_UC_D1_MAC2_MASK		GENMASK(15, 8)
+#define RTL8365MB_L2_UC_D2_MAC1_MASK		GENMASK(7, 0)
+#define RTL8365MB_L2_UC_D2_MAC0_MASK		GENMASK(15, 8)
+#define RTL8365MB_L2_UC_D3_VID_MASK		GENMASK(11, 0)
+#define RTL8365MB_L2_UC_D3_IVL_MASK		GENMASK(13, 13)
+#define RTL8365MB_L2_UC_D3_PORT_EXT_MASK	GENMASK(15, 15)
+#define RTL8365MB_L2_UC_D4_EFID_MASK		GENMASK(2, 0)
+#define RTL8365MB_L2_UC_D4_FID_MASK		GENMASK(6, 3)
+#define RTL8365MB_L2_UC_D4_SA_PRI_MASK		GENMASK(7, 7)
+#define RTL8365MB_L2_UC_D4_PORT_MASK		GENMASK(10, 8)
+#define RTL8365MB_L2_UC_D4_AGE_MASK		GENMASK(13, 11)
+#define RTL8365MB_L2_UC_D4_AUTH_MASK		GENMASK(14, 14)
+#define RTL8365MB_L2_UC_D4_SA_BLOCK_MASK	GENMASK(15, 15)
+#define RTL8365MB_L2_UC_D5_DA_BLOCK_MASK	GENMASK(0, 0)
+#define RTL8365MB_L2_UC_D5_PRIORITY_MASK	GENMASK(3, 1)
+#define RTL8365MB_L2_UC_D5_FWD_PRI_MASK		GENMASK(4, 4)
+#define RTL8365MB_L2_UC_D5_STATIC_MASK		GENMASK(5, 5)
+
+#define RTL8365MB_L2_MC_MAC5_MASK		GENMASK(7, 0)   /* D0 */
+#define RTL8365MB_L2_MC_MAC4_MASK		GENMASK(15, 8)  /* D0 */
+#define RTL8365MB_L2_MC_MAC3_MASK		GENMASK(7, 0)   /* D1 */
+#define RTL8365MB_L2_MC_MAC2_MASK		GENMASK(15, 8)  /* D1 */
+#define RTL8365MB_L2_MC_MAC1_MASK		GENMASK(7, 0)   /* D2 */
+#define RTL8365MB_L2_MC_MAC0_MASK		GENMASK(15, 8)  /* D2 */
+#define RTL8365MB_L2_MC_VID_MASK		GENMASK(11, 0)  /* D3 */
+#define RTL8365MB_L2_MC_IVL_MASK		GENMASK(13, 13) /* D3 */
+#define RTL8365MB_L2_MC_MBR_EXT1_MASK		GENMASK(15, 14) /* D3 */
+
+#define RTL8365MB_L2_MC_MBR_MASK		GENMASK(7, 0)   /* D4 */
+#define RTL8365MB_L2_MC_IGMPIDX_MASK		GENMASK(15, 8)  /* D4 */
+
+#define RTL8365MB_L2_MC_IGMP_ASIC_MASK		GENMASK(0, 0)   /* D5 */
+#define RTL8365MB_L2_MC_PRIORITY_MASK		GENMASK(3, 1)   /* D5 */
+#define RTL8365MB_L2_MC_FWD_PRI_MASK		GENMASK(4, 4)   /* D5 */
+#define RTL8365MB_L2_MC_STATIC_MASK		GENMASK(5, 5)   /* D5 */
+#define RTL8365MB_L2_MC_MBR_EXT2_MASK		GENMASK(7, 7)   /* D5 */
+
+/* Port flush command registers - writing a 1 to the port's MASK bit will
+ * initiate the flush procedure. Completion is signalled when the corresponding
+ * BUSY bit is 0.
+ */
+#define RTL8365MB_L2_FLUSH_PORT_REG		0x0A36
+#define   RTL8365MB_L2_FLUSH_PORT_MASK_MASK	GENMASK(7, 0)
+#define   RTL8365MB_L2_FLUSH_PORT_BUSY_MASK	GENMASK(15, 8)
+
+#define RTL8365MB_L2_FLUSH_PORT_EXT_REG		0x0A35
+#define   RTL8365MB_L2_FLUSH_PORT_EXT_MASK_MASK	GENMASK(2, 0)
+#define   RTL8365MB_L2_FLUSH_PORT_EXT_BUSY_MASK	GENMASK(5, 3)
+
+#define RTL8365MB_L2_FLUSH_CTRL1_REG		0x0A37
+#define   RTL8365MB_L2_FLUSH_CTRL1_VID_MASK	GENMASK(11, 0)
+#define   RTL8365MB_L2_FLUSH_CTRL1_FID_MASK	GENMASK(15, 12)
+
+#define RTL8365MB_L2_FLUSH_CTRL2_REG		0x0A38
+#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_MASK	GENMASK(1, 0)
+#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT	0
+#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_VID 1
+#define   RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_FID 2
+#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_MASK	GENMASK(2, 2)
+#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_DYNAMIC	0
+#define   RTL8365MB_L2_FLUSH_CTRL2_TYPE_BOTH	0
+
+/* This flushes the entire LUT, reading it back it will turn 0 when the
+ * operation is complete
+ */
+#define RTL8365MB_L2_FLUSH_CTRL3_REG		0x0A39
+#define   RTL8365MB_L2_FLUSH_CTRL3_MASK		GENMASK(0, 0)
+
+struct rtl8365mb_l2_mc_key {
+	u8 mac_addr[ETH_ALEN];
+	union {
+		u16 vid; /* IVL */
+		u16 fid; /* SVL */
+	};
+	bool ivl;
+};
+
+struct rtl8365mb_l2_mc {
+	struct rtl8365mb_l2_mc_key key;
+	u16 member;
+	u8 priority;
+	u8 igmpidx;
+
+	bool is_static;
+	bool fwd_pri;
+	bool igmp_asic;
+};
+
+static void rtl8365mb_l2_data_to_uc(const u16 *data, struct rtl8365mb_l2_uc *uc)
+{
+	uc->key.mac_addr[5] = FIELD_GET(RTL8365MB_L2_UC_D0_MAC5_MASK, data[0]);
+	uc->key.mac_addr[4] = FIELD_GET(RTL8365MB_L2_UC_D0_MAC4_MASK, data[0]);
+	uc->key.mac_addr[3] = FIELD_GET(RTL8365MB_L2_UC_D1_MAC3_MASK, data[1]);
+	uc->key.mac_addr[2] = FIELD_GET(RTL8365MB_L2_UC_D1_MAC2_MASK, data[1]);
+	uc->key.mac_addr[1] = FIELD_GET(RTL8365MB_L2_UC_D2_MAC1_MASK, data[2]);
+	uc->key.mac_addr[0] = FIELD_GET(RTL8365MB_L2_UC_D2_MAC0_MASK, data[2]);
+	uc->key.efid = FIELD_GET(RTL8365MB_L2_UC_D4_EFID_MASK, data[4]);
+	uc->key.vid = FIELD_GET(RTL8365MB_L2_UC_D3_VID_MASK, data[3]);
+	uc->key.ivl = FIELD_GET(RTL8365MB_L2_UC_D3_IVL_MASK, data[3]);
+	uc->key.fid = FIELD_GET(RTL8365MB_L2_UC_D4_FID_MASK, data[4]);
+	uc->age = FIELD_GET(RTL8365MB_L2_UC_D4_AGE_MASK, data[4]);
+	uc->auth = FIELD_GET(RTL8365MB_L2_UC_D4_AUTH_MASK, data[4]);
+	uc->port = FIELD_GET(RTL8365MB_L2_UC_D4_PORT_MASK, data[4]) |
+		   (FIELD_GET(RTL8365MB_L2_UC_D3_PORT_EXT_MASK, data[3]) << 3);
+	uc->sa_pri = FIELD_GET(RTL8365MB_L2_UC_D4_SA_PRI_MASK, data[4]);
+	uc->fwd_pri = FIELD_GET(RTL8365MB_L2_UC_D5_FWD_PRI_MASK, data[5]);
+	uc->sa_block = FIELD_GET(RTL8365MB_L2_UC_D4_SA_BLOCK_MASK, data[4]);
+	uc->da_block = FIELD_GET(RTL8365MB_L2_UC_D5_DA_BLOCK_MASK, data[5]);
+	uc->priority = FIELD_GET(RTL8365MB_L2_UC_D5_PRIORITY_MASK, data[5]);
+	uc->is_static = FIELD_GET(RTL8365MB_L2_UC_D5_STATIC_MASK, data[5]);
+}
+
+static void rtl8365mb_l2_uc_to_data(const struct rtl8365mb_l2_uc *uc, u16 *data)
+{
+	memset(data, 0, RTL8365MB_L2_ENTRY_SIZE * 2);
+	data[0] |=
+		FIELD_PREP(RTL8365MB_L2_UC_D0_MAC5_MASK, uc->key.mac_addr[5]);
+	data[0] |=
+		FIELD_PREP(RTL8365MB_L2_UC_D0_MAC4_MASK, uc->key.mac_addr[4]);
+	data[1] |=
+		FIELD_PREP(RTL8365MB_L2_UC_D1_MAC3_MASK, uc->key.mac_addr[3]);
+	data[1] |=
+		FIELD_PREP(RTL8365MB_L2_UC_D1_MAC2_MASK, uc->key.mac_addr[2]);
+	data[2] |=
+		FIELD_PREP(RTL8365MB_L2_UC_D2_MAC1_MASK, uc->key.mac_addr[1]);
+	data[2] |=
+		FIELD_PREP(RTL8365MB_L2_UC_D2_MAC0_MASK, uc->key.mac_addr[0]);
+	data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_VID_MASK, uc->key.vid);
+	data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_IVL_MASK, uc->key.ivl);
+	data[3] |= FIELD_PREP(RTL8365MB_L2_UC_D3_PORT_EXT_MASK, uc->port >> 3);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_FID_MASK, uc->key.fid);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_EFID_MASK, uc->key.efid);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_AGE_MASK, uc->age);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_AUTH_MASK, uc->auth);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_PORT_MASK, uc->port);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_SA_PRI_MASK, uc->sa_pri);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_UC_D4_SA_BLOCK_MASK, uc->sa_block);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_FWD_PRI_MASK, uc->fwd_pri);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_DA_BLOCK_MASK, uc->da_block);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_PRIORITY_MASK, uc->priority);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_UC_D5_STATIC_MASK, uc->is_static);
+}
+
+static void rtl8365mb_l2_data_to_mc(const u16 *data, struct rtl8365mb_l2_mc *mc)
+{
+	mc->key.mac_addr[5] = FIELD_GET(RTL8365MB_L2_MC_MAC5_MASK, data[0]);
+	mc->key.mac_addr[4] = FIELD_GET(RTL8365MB_L2_MC_MAC4_MASK, data[0]);
+	mc->key.mac_addr[3] = FIELD_GET(RTL8365MB_L2_MC_MAC3_MASK, data[1]);
+	mc->key.mac_addr[2] = FIELD_GET(RTL8365MB_L2_MC_MAC2_MASK, data[1]);
+	mc->key.mac_addr[1] = FIELD_GET(RTL8365MB_L2_MC_MAC1_MASK, data[2]);
+	mc->key.mac_addr[0] = FIELD_GET(RTL8365MB_L2_MC_MAC0_MASK, data[2]);
+	mc->key.vid = FIELD_GET(RTL8365MB_L2_MC_VID_MASK, data[3]);
+	mc->key.ivl = FIELD_GET(RTL8365MB_L2_MC_IVL_MASK, data[3]);
+	mc->priority = FIELD_GET(RTL8365MB_L2_MC_PRIORITY_MASK, data[5]);
+	mc->fwd_pri = FIELD_GET(RTL8365MB_L2_MC_FWD_PRI_MASK, data[5]);
+	mc->is_static = FIELD_GET(RTL8365MB_L2_MC_STATIC_MASK, data[5]);
+	mc->member = FIELD_GET(RTL8365MB_L2_MC_MBR_MASK, data[4]) |
+		     (FIELD_GET(RTL8365MB_L2_MC_MBR_EXT1_MASK, data[3]) << 8) |
+		     (FIELD_GET(RTL8365MB_L2_MC_MBR_EXT2_MASK, data[5]) << 8);
+	mc->igmpidx = FIELD_GET(RTL8365MB_L2_MC_IGMPIDX_MASK, data[4]);
+	mc->igmp_asic = FIELD_GET(RTL8365MB_L2_MC_IGMP_ASIC_MASK, data[5]);
+}
+
+static void rtl8365mb_l2_mc_to_data(const struct rtl8365mb_l2_mc *mc, u16 *data)
+{
+	memset(data, 0, 12);
+	data[0] |= FIELD_PREP(RTL8365MB_L2_MC_MAC5_MASK, mc->key.mac_addr[5]);
+	data[0] |= FIELD_PREP(RTL8365MB_L2_MC_MAC4_MASK, mc->key.mac_addr[4]);
+	data[1] |= FIELD_PREP(RTL8365MB_L2_MC_MAC3_MASK, mc->key.mac_addr[3]);
+	data[1] |= FIELD_PREP(RTL8365MB_L2_MC_MAC2_MASK, mc->key.mac_addr[2]);
+	data[2] |= FIELD_PREP(RTL8365MB_L2_MC_MAC1_MASK, mc->key.mac_addr[1]);
+	data[2] |= FIELD_PREP(RTL8365MB_L2_MC_MAC0_MASK, mc->key.mac_addr[0]);
+	data[3] |= FIELD_PREP(RTL8365MB_L2_MC_VID_MASK, mc->key.vid);
+	data[3] |= FIELD_PREP(RTL8365MB_L2_MC_IVL_MASK, mc->key.ivl);
+	data[3] |= FIELD_PREP(RTL8365MB_L2_MC_MBR_EXT1_MASK, mc->member >> 8);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_MC_MBR_MASK, mc->member);
+	data[4] |= FIELD_PREP(RTL8365MB_L2_MC_IGMPIDX_MASK, mc->igmpidx);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_MC_IGMP_ASIC_MASK, mc->igmp_asic);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_MC_PRIORITY_MASK, mc->priority);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_MC_FWD_PRI_MASK, mc->fwd_pri);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_MC_STATIC_MASK, mc->is_static);
+	data[5] |= FIELD_PREP(RTL8365MB_L2_MC_MBR_EXT2_MASK, mc->member >> 10);
+}
+
+/**
+ * rtl8365mb_l2_get_next_uc() - get the next Unicast L2 entry
+ *
+ * @priv: realtek_priv pointer
+ * @addr: as input, the table index to start the walk
+ *        as output, the found table index
+ * @port: restrict the walk on entries related to port
+ * @uc: returned L2 Unicast table entry
+ *
+ * This function get the next unicast L2 table entry starting from @addr
+ * and checking exclusively entries related to @port. If no more entries
+ * were found, the output @addr will be lower than the input @addr and @uc
+ * will not be overwritten.
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ **/
+int rtl8365mb_l2_get_next_uc(struct realtek_priv *priv, u16 *addr, u16 port,
+			     struct rtl8365mb_l2_uc *uc)
+{
+	u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
+	int ret;
+
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_READ, addr,
+				    RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT,
+				    port, data, RTL8365MB_L2_ENTRY_SIZE);
+	if (ret)
+		return ret;
+
+	rtl8365mb_l2_data_to_uc(data, uc);
+
+	return 0;
+}
+
+int rtl8365mb_l2_add_uc(struct realtek_priv *priv, u16 port,
+			const unsigned char mac_addr[static ETH_ALEN],
+			u16 efid, u16 vid)
+{
+	u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
+	struct rtl8365mb_l2_uc uc = { 0 };
+	u16 addr;
+	int ret;
+
+	memcpy(uc.key.mac_addr, mac_addr, ETH_ALEN);
+	uc.key.efid = efid;
+	uc.key.ivl = true;
+	uc.key.vid = vid;
+	uc.port = port;
+	/* do not let HW decrease age */
+	uc.is_static = true;
+	/* age greater than 0 adds/updates entries */
+	uc.age = 1;
+	rtl8365mb_l2_uc_to_data(&uc, data);
+
+	/* add the new entry or update an existing one */
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_WRITE, &addr,
+				    0, 0,
+				    data, RTL8365MB_L2_ENTRY_SIZE);
+	/* addr will hold the table index, but it is not used here */
+	if (ret == -ENOENT) {
+		/* -ENOENT means the just added entry was not found (and @addr
+		 * does not hold the table index. Although any error will be
+		 * treated equally by the caller, assume that the missing entry
+		 * means the table is full (tested in real HW).
+		 */
+		return -ENOSPC;
+	}
+	return ret;
+}
+
+int rtl8365mb_l2_del_uc(struct realtek_priv *priv, u16 port,
+			const unsigned char mac_addr[static ETH_ALEN],
+			u16 efid, u16 vid)
+{
+	u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
+	struct rtl8365mb_l2_uc uc = { 0 };
+	u16 addr;
+	int ret;
+
+	memcpy(uc.key.mac_addr, mac_addr, ETH_ALEN);
+	uc.key.efid = efid;
+	uc.key.ivl = true;
+	uc.key.vid = vid;
+	/* age 0 deletes the entry */
+	uc.age = 0;
+	rtl8365mb_l2_uc_to_data(&uc, data);
+
+	/* it looks like the switch will always add/update the entry,
+	 * even when age is 0 or uc.key did not match an existing entry,
+	 * just to immediately drop it because age is zero. You can still
+	 * get the added/updated address from @addr
+	 */
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_WRITE, &addr,
+				    0, 0,
+				    data, RTL8365MB_L2_ENTRY_SIZE);
+	/* addr will hold the table index, but it is not used here */
+	return ret;
+}
+
+int rtl8365mb_l2_flush(struct realtek_priv *priv, int port, u16 vid)
+{
+	int mode = vid ? RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT_VID :
+			 RTL8365MB_L2_FLUSH_CTRL2_MODE_PORT;
+	u32 val, mask;
+	int ret;
+
+	mutex_lock(&priv->map_lock);
+
+	/* Configure flushing mode; only flush dynamic entries */
+	ret = regmap_write(priv->map_nolock, RTL8365MB_L2_FLUSH_CTRL2_REG,
+			   FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL2_MODE_MASK,
+				      mode) |
+			   FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL2_TYPE_MASK,
+				      RTL8365MB_L2_FLUSH_CTRL2_TYPE_DYNAMIC));
+	if (ret)
+		goto out;
+
+	ret = regmap_write(priv->map_nolock, RTL8365MB_L2_FLUSH_CTRL1_REG,
+			   FIELD_PREP(RTL8365MB_L2_FLUSH_CTRL1_VID_MASK, vid));
+
+	if (ret)
+		goto out;
+	/* Now issue the flush command and wait for its completion. There are
+	 * two registers for this purpose, and which one to use depends on the
+	 * port number. The _EXT register is for ports 8 or higher.
+	 */
+	if (port < 8) {
+		val = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_MASK_MASK,
+				 BIT(port) & 0xFF);
+		ret = regmap_write(priv->map_nolock,
+				   RTL8365MB_L2_FLUSH_PORT_REG, val);
+		if (ret)
+			goto out;
+
+		mask = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_BUSY_MASK,
+				  BIT(port) & 0xFF);
+		ret = regmap_read_poll_timeout(priv->map_nolock,
+					       RTL8365MB_L2_FLUSH_PORT_REG,
+					       val, !(val & mask), 10, 100);
+		if (ret)
+			goto out;
+	} else {
+		val = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_EXT_MASK_MASK,
+				 BIT(port) >> 8);
+		ret = regmap_write(priv->map_nolock,
+				   RTL8365MB_L2_FLUSH_PORT_EXT_REG, val);
+		if (ret)
+			goto out;
+
+		mask = FIELD_PREP(RTL8365MB_L2_FLUSH_PORT_EXT_BUSY_MASK,
+				  BIT(port) >> 8);
+		ret = regmap_read_poll_timeout(priv->map_nolock,
+					       RTL8365MB_L2_FLUSH_PORT_EXT_REG,
+					       val, !(val & mask), 10, 100);
+		if (ret)
+			goto out;
+	}
+
+out:
+	mutex_unlock(&priv->map_lock);
+
+	return ret;
+}
+
+int rtl8365mb_l2_add_mc(struct realtek_priv *priv, u16 port,
+			const unsigned char mac_addr[static ETH_ALEN],
+			u16 vid)
+{
+	u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
+	struct rtl8365mb_l2_mc mc = { 0 };
+	u16 addr;
+	int ret;
+
+	memcpy(mc.key.mac_addr, mac_addr, ETH_ALEN);
+	mc.key.vid = vid;
+	mc.key.ivl = true;
+	/* Already set the port and is_static, although not used in OP_READ,
+	 * data will be ready for OP_WRITE if it is a new entry.
+	 */
+	mc.member |= BIT(port);
+	mc.is_static = 1;
+	rtl8365mb_l2_mc_to_data(&mc, data);
+
+	/* First look for an existing entry (to get existing port members) */
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_READ, &addr,
+				    RTL8365MB_TABLE_L2_METHOD_MAC, 0,
+				    data, RTL8365MB_L2_ENTRY_SIZE);
+	if (!ret) {
+		/* There is already an entry... */
+		rtl8365mb_l2_data_to_mc(data, &mc);
+		/* the port must be added as a member */
+		mc.member |= BIT(port);
+		rtl8365mb_l2_mc_to_data(&mc, data);
+	} else if (ret == -ENOENT) {
+		/* New entry, no need to update data again as it already
+		 * includes the member
+		 */
+	} else {
+		return ret;
+	}
+
+	/* add the new entry or update an existing one */
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_WRITE, &addr,
+				    0, 0,
+				    data, RTL8365MB_L2_ENTRY_SIZE);
+	/* addr will hold the table index, but it is not used here */
+	if (ret == -ENOENT) {
+		/* -ENOENT means the just added entry was not found (and @addr
+		 * does not hold the table index. Although any error will be
+		 * treated equally by the caller, assume that the missing entry
+		 * means the table is full (tested in real HW).
+		 */
+		return -ENOSPC;
+	}
+
+	return ret;
+}
+
+int rtl8365mb_l2_del_mc(struct realtek_priv *priv, u16 port,
+			const unsigned char mac_addr[static ETH_ALEN],
+			u16 vid)
+{
+	u16 data[RTL8365MB_L2_ENTRY_SIZE] = { 0 };
+	struct rtl8365mb_l2_mc mc = { 0 };
+	u16 addr;
+	int ret;
+
+	memcpy(mc.key.mac_addr, mac_addr, ETH_ALEN);
+	mc.key.vid = vid;
+	mc.key.ivl = true;
+	rtl8365mb_l2_mc_to_data(&mc, data);
+
+	/* First look for an existing entry (to get existing port members) */
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_READ, &addr,
+				    RTL8365MB_TABLE_L2_METHOD_MAC, 0,
+				    data, RTL8365MB_L2_ENTRY_SIZE);
+	if (ret)
+		/* Any error, including -ENOENT is unexpected */
+		return ret;
+
+	rtl8365mb_l2_data_to_mc(data, &mc);
+	/* the port must be removed as a member */
+	mc.member &= ~BIT(port);
+	if (!mc.member) {
+		/* With no members, zero all non-key fields to delete the
+		 * entry. However is_static is everything else we wrote.
+		 * (and probably all that is needed by the HW)
+		 */
+		mc.is_static = 0;
+	}
+	rtl8365mb_l2_mc_to_data(&mc, data);
+
+	/* update the existing entry. */
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_L2,
+				    RTL8365MB_TABLE_OP_WRITE, &addr,
+				    0, 0,
+				    data, RTL8365MB_L2_ENTRY_SIZE);
+	return ret;
+}
diff --git a/drivers/net/dsa/realtek/rtl8365mb_l2.h b/drivers/net/dsa/realtek/rtl8365mb_l2.h
new file mode 100644
index 000000000000..b60d8617969d
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_l2.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Forwarding and multicast database interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#ifndef _REALTEK_RTL8365MB_L2_H
+#define _REALTEK_RTL8365MB_L2_H
+
+#include <linux/if_ether.h>
+#include <linux/types.h>
+
+#include "realtek.h"
+
+/* It's valid for all family but RTL8370B, which has 4160 */
+#define RTL8365MB_LEARN_LIMIT_MAX	2112
+
+struct rtl8365mb_l2_uc_key {
+	u8 mac_addr[ETH_ALEN];
+	union {
+		u16 vid; /* IVL */
+		u16 fid; /* SVL */
+	};
+	bool ivl;
+	u16 efid;
+};
+
+struct rtl8365mb_l2_uc {
+	struct rtl8365mb_l2_uc_key key;
+	u8 port;
+	u8 age;
+	u8 priority;
+
+	bool sa_block;
+	bool da_block;
+	bool auth;
+	bool is_static;
+	bool sa_pri;
+	bool fwd_pri;
+};
+
+int rtl8365mb_l2_get_next_uc(struct realtek_priv *priv, u16 *addr, u16 port,
+			     struct rtl8365mb_l2_uc *uc);
+int rtl8365mb_l2_add_uc(struct realtek_priv *priv, u16 port,
+			const unsigned char addr[static ETH_ALEN],
+			u16 efid, u16 vid);
+int rtl8365mb_l2_del_uc(struct realtek_priv *priv, u16 port,
+			const unsigned char addr[static ETH_ALEN],
+			u16 efid, u16 vid);
+int rtl8365mb_l2_flush(struct realtek_priv *priv, int port, u16 vid);
+
+int rtl8365mb_l2_add_mc(struct realtek_priv *priv, u16 port,
+			const unsigned char mac_addr[static ETH_ALEN],
+			u16 vid);
+int rtl8365mb_l2_del_mc(struct realtek_priv *priv, u16 port,
+			const unsigned char mac_addr[static ETH_ALEN],
+			u16 vid);
+
+#endif /* _REALTEK_RTL8365MB_L2_H */
diff --git a/drivers/net/dsa/realtek/rtl8365mb_main.c b/drivers/net/dsa/realtek/rtl8365mb_main.c
index c955864f17b3..0767b2704ad7 100644
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -104,6 +104,7 @@
 #include "realtek-smi.h"
 #include "realtek-mdio.h"
 #include "rtl83xx.h"
+#include "rtl8365mb_l2.h"
 #include "rtl8365mb_vlan.h"
 
 /* Family-specific data and limits */
@@ -112,7 +113,6 @@
 #define RTL8365MB_PHYREGMAX		(RTL8365MB_NUM_PHYREGS - 1)
 #define RTL8365MB_MAX_NUM_PORTS		11
 #define RTL8365MB_MAX_NUM_EXTINTS	3
-#define RTL8365MB_LEARN_LIMIT_MAX	2112
 
 /* Chip identification registers */
 #define RTL8365MB_CHIP_ID_REG		0x1300
@@ -705,6 +705,7 @@ struct rtl8365mb_port {
  * @chip_info: chip-specific info about the attached switch
  * @cpu: CPU tagging and CPU port configuration for this chip
  * @mib_lock: prevent concurrent reads of MIB counters
+ * @l2_lock: prevent concurrent access to L2 look-up table
  * @ports: per-port data
  *
  * Private data for this driver.
@@ -715,6 +716,15 @@ struct rtl8365mb {
 	const struct rtl8365mb_chip_info *chip_info;
 	struct rtl8365mb_cpu cpu;
 	struct mutex mib_lock;
+	/* l2_lock is used to prevent concurrent modifications of L2 table
+	 * entries while another function is reading it. l2_(add,del)_mc
+	 * is an example that first read current table entry and then
+	 * create/update it. l2_(add|del)_uc uses a single table op and,
+	 * internally, it might not need this lock. However, altering FDB
+	 * may still collide, as well as l2_flush, with fdb_dump iterating
+	 * over FDB.
+	 */
+	struct mutex l2_lock;
 	struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
 };
 
@@ -1267,6 +1277,22 @@ static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
 			   val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
 }
 
+static void rtl8365mb_port_fast_age(struct dsa_switch *ds, int port)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb *mb = priv->chip_data;
+	int ret;
+
+	dev_dbg(priv->dev, "fast_age port %d\n", port);
+
+	mutex_lock(&mb->l2_lock);
+	ret = rtl8365mb_l2_flush(priv, port, 0);
+	mutex_unlock(&mb->l2_lock);
+	if (ret)
+		dev_err(priv->dev, "failed to fast age on port %d: %d\n", port,
+			ret);
+}
+
 static int rtl8365mb_port_set_transparent(struct realtek_priv *priv,
 					  int igr_port, int egr_port,
 					  bool enable)
@@ -1444,6 +1470,165 @@ static int rtl8365mb_vlan_setup(struct dsa_switch *ds)
 	return ret;
 }
 
+static int rtl8365mb_port_fdb_add(struct dsa_switch *ds, int port,
+				  const unsigned char *addr, u16 vid,
+				  struct dsa_db db)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb *mb;
+	int efid;
+	int ret;
+
+	mb = priv->chip_data;
+
+	if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
+		return -EOPNOTSUPP;
+
+	/*
+	 * DSA_DB_BRIDGE ports use bridge number [1..N] as EFID, while
+	 * DSA_DB_PORT use the default EFID (0), not used by any bridge.
+	 */
+	efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
+
+	dev_dbg(priv->dev, "fdb_add port %d addr %pM efid %d vid %d\n",
+		port, addr, efid, vid);
+
+	mutex_lock(&mb->l2_lock);
+	ret = rtl8365mb_l2_add_uc(priv, port, addr, efid, vid);
+	mutex_unlock(&mb->l2_lock);
+
+	if (ret)
+		dev_err(priv->dev, "fdb_add ERROR %pe\n", ERR_PTR(ret));
+	return ret;
+}
+
+static int rtl8365mb_port_fdb_del(struct dsa_switch *ds, int port,
+				  const unsigned char *addr, u16 vid,
+				  struct dsa_db db)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb *mb;
+	int efid;
+	int ret;
+
+	mb = priv->chip_data;
+
+	if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
+		return -EOPNOTSUPP;
+
+	/*
+	 * DSA_DB_BRIDGE ports use bridge number [1..N] as EFID, while
+	 * DSA_DB_PORT use the default EFID (0), not used by any bridge.
+	 */
+	efid = db.type == DSA_DB_BRIDGE ? db.bridge.num : 0;
+
+	dev_dbg(priv->dev, "fdb_del port %d addr %pM efid %d vid %d\n",
+		port, addr, efid, vid);
+
+	mutex_lock(&mb->l2_lock);
+	ret = rtl8365mb_l2_del_uc(priv, port, addr, efid, vid);
+	mutex_unlock(&mb->l2_lock);
+
+	if (ret)
+		dev_err(priv->dev, "fdb_del ERROR %pe\n", ERR_PTR(ret));
+	return ret;
+}
+
+static int rtl8365mb_port_fdb_dump(struct dsa_switch *ds, int port,
+				   dsa_fdb_dump_cb_t *cb, void *data)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb_l2_uc uc = {0};
+	u16 start_addr, addr = 0;
+	struct rtl8365mb *mb;
+	int ret = 0;
+
+	mb = priv->chip_data;
+
+	mutex_lock(&mb->l2_lock);
+	while (true) {
+		start_addr = addr;
+
+		dev_dbg(priv->dev, "l2_get_next_uc, addr:%d, port:%d\n",
+			addr, port);
+		ret = rtl8365mb_l2_get_next_uc(priv, &addr, port, &uc);
+		dev_dbg(priv->dev,
+			"l2_get_next_uc addr:%d mac:%pM vid:%d static:%d ret:%pe\n",
+			addr, uc.key.mac_addr, uc.key.vid, uc.is_static,
+			ERR_PTR(ret));
+
+		/* table is empty for that port */
+		if (ret == -ENOENT)
+			break;
+		if (ret)
+			break;
+
+		/* overflow: reached the end */
+		if (addr < start_addr)
+			break;
+
+		cb(uc.key.mac_addr, uc.key.vid, uc.is_static, data);
+
+		addr++;
+
+		/* Avoid -ETIMEDOUT in rtl8365mb_l2_get_next_uc() */
+		if (addr > RTL8365MB_LEARN_LIMIT_MAX)
+			break;
+	}
+	mutex_unlock(&mb->l2_lock);
+
+	return ret;
+}
+
+static int rtl8365mb_port_mdb_add(struct dsa_switch *ds, int port,
+				  const struct switchdev_obj_port_mdb *mdb,
+				  struct dsa_db db)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb *mb;
+	int ret;
+
+	mb = priv->chip_data;
+
+	if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
+		return -EOPNOTSUPP;
+
+	dev_dbg(priv->dev, "mdb_add port %d addr %pM vid %d\n",
+		port, mdb->addr, mdb->vid);
+
+	mutex_lock(&mb->l2_lock);
+	ret = rtl8365mb_l2_add_mc(priv, port, mdb->addr, mdb->vid);
+	mutex_unlock(&mb->l2_lock);
+
+	if (ret)
+		dev_err(priv->dev, "mdb_add ERROR %pe\n", ERR_PTR(ret));
+	return ret;
+}
+
+static int rtl8365mb_port_mdb_del(struct dsa_switch *ds, int port,
+				  const struct switchdev_obj_port_mdb *mdb,
+				  struct dsa_db db)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb *mb;
+	int ret;
+
+	mb = priv->chip_data;
+
+	if (db.type != DSA_DB_PORT && db.type != DSA_DB_BRIDGE)
+		return -EOPNOTSUPP;
+
+	dev_dbg(priv->dev, "mdb_del port %d addr %pM vid %d\n",
+		port, mdb->addr, mdb->vid);
+
+	mutex_lock(&mb->l2_lock);
+	ret = rtl8365mb_l2_del_mc(priv, port, mdb->addr, mdb->vid);
+	mutex_unlock(&mb->l2_lock);
+
+	if (ret)
+		dev_err(priv->dev, "mdb_del ERROR %pe\n", ERR_PTR(ret));
+	return ret;
+}
 static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
 				       bool enable)
 {
@@ -2305,6 +2490,8 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
 	mb = priv->chip_data;
 	cpu = &mb->cpu;
 
+	mutex_init(&mb->l2_lock);
+
 	ret = rtl8365mb_reset_chip(priv);
 	if (ret) {
 		dev_err(priv->dev, "failed to reset chip: %d\n", ret);
@@ -2369,6 +2556,8 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
 	if (ret)
 		goto out_teardown_irq;
 
+	ds->assisted_learning_on_cpu_port = true;
+	ds->fdb_isolation = true;
 	/* The EFID is 3 bits, but EFID 0 is reserved for standalone ports */
 	ds->max_num_bridges = FIELD_MAX(RTL8365MB_EFID_MASK);
 
@@ -2498,6 +2687,12 @@ static const struct dsa_switch_ops rtl8365mb_switch_ops = {
 	.port_bridge_join = rtl8365mb_port_bridge_join,
 	.port_bridge_leave = rtl8365mb_port_bridge_leave,
 	.port_stp_state_set = rtl8365mb_port_stp_state_set,
+	.port_fast_age = rtl8365mb_port_fast_age,
+	.port_fdb_add = rtl8365mb_port_fdb_add,
+	.port_fdb_del = rtl8365mb_port_fdb_del,
+	.port_fdb_dump = rtl8365mb_port_fdb_dump,
+	.port_mdb_add = rtl8365mb_port_mdb_add,
+	.port_mdb_del = rtl8365mb_port_mdb_del,
 	.port_vlan_add = rtl8365mb_port_vlan_add,
 	.port_vlan_del = rtl8365mb_port_vlan_del,
 	.port_vlan_filtering = rtl8365mb_port_vlan_filtering,

-- 
2.53.0


^ permalink raw reply related

* [net-next PATCH 07/10] net: dsa: realtek: rtl8365mb: add port_bridge_{join,leave}
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
	Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
  Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>

From: Alvin Šipraga <alsi@bang-olufsen.dk>

Implement hardware offloading of bridge functionality. This is achieved
by using the per-port isolation registers, which contain a forwarding
port mask. The switch will refuse to forward packets ingressed on a
given port to a port which is not in its forwarding mask.

For each bridge that is offloaded, use the DSA-provided bridge number
for the Extended Filtering ID (EFID). When using Independent VLAN
Learning (IVL), the forwarding database is keyed with the tuple
{VID, MAC, EFID}. There are 8 EFIDs available (0~7), but we reserve the
default EFID 0 for standalone ports where learning is disabled. This
fits nicely because DSA indexes the bridge number starting from 1.

Because of the limited number of EFIDs, we have to set the
max_num_bridges property of our switch to 7: we can't offload more than
that or we will fail to offer IVL as at least two bridges would end up
having to share an EFID.

Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
 drivers/net/dsa/realtek/rtl8365mb_main.c | 123 ++++++++++++++++++++++++++++++-
 1 file changed, 122 insertions(+), 1 deletion(-)

diff --git a/drivers/net/dsa/realtek/rtl8365mb_main.c b/drivers/net/dsa/realtek/rtl8365mb_main.c
index c604bd744d38..c955864f17b3 100644
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -285,6 +285,15 @@
 		(RTL8365MB_PORT_ISOLATION_REG_BASE + (_physport))
 #define   RTL8365MB_PORT_ISOLATION_MASK			0x07FF
 
+/* Extended filter ID registers - used to key forwarding database with IVL */
+#define RTL8365MB_EFID_MASK			GENMASK(2, 0)
+#define RTL8365MB_PORT_EFID_REG_BASE		0x0A32
+#define RTL8365MB_PORT_EFID_REG(_p) \
+		(RTL8365MB_PORT_EFID_REG_BASE + ((_p) >> 2))
+#define   RTL8365MB_PORT_EFID_OFFSET(_p)	(((_p) & 0x3) << 2)
+#define   RTL8365MB_PORT_EFID_MASK(_p) \
+		(RTL8365MB_EFID_MASK << RTL8365MB_PORT_EFID_OFFSET(_p))
+
 /* MSTP port state registers - indexed by tree instance */
 #define RTL8365MB_MSTI_CTRL_BASE			0x0A00
 #define RTL8365MB_MSTI_CTRL_REG(_msti, _physport) \
@@ -1447,10 +1456,117 @@ static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
 			    enable ? RTL8365MB_LEARN_LIMIT_MAX : 0);
 }
 
+static int rtl8365mb_port_set_efid(struct realtek_priv *priv, int port,
+				   u32 efid)
+{
+	return regmap_update_bits(priv->map, RTL8365MB_PORT_EFID_REG(port),
+				  RTL8365MB_PORT_EFID_MASK(port),
+				  efid << RTL8365MB_PORT_EFID_OFFSET(port));
+}
+
+/* Port isolation manipulation functions.
+ *
+ * The port isolation register controls the forwarding mask of a given
+ * port. The switch will not forward packets ingressed on a given port
+ * to ports which are not enabled in its forwarding mask.
+ *
+ * The port forwarding mask has the highest priority in forwarding
+ * decisions. The only exception to this rule is when the switch
+ * receives a packet on its CPU port with ALLOW=0. In that case the TX
+ * field of the CPU tag will override the forwarding port mask.
+ */
 static int rtl8365mb_port_set_isolation(struct realtek_priv *priv, int port,
 					u32 mask)
 {
-	return regmap_write(priv->map, RTL8365MB_PORT_ISOLATION_REG(port), mask);
+	return regmap_write(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+			    mask);
+}
+
+static int rtl8365mb_port_add_isolation(struct realtek_priv *priv, int port,
+					u32 mask)
+{
+	return regmap_update_bits(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+				  mask, mask);
+}
+
+static int rtl8365mb_port_remove_isolation(struct realtek_priv *priv, int port,
+					   u32 mask)
+{
+	return regmap_update_bits(priv->map, RTL8365MB_PORT_ISOLATION_REG(port),
+				  mask, 0);
+}
+
+static int rtl8365mb_port_bridge_join(struct dsa_switch *ds, int port,
+				      struct dsa_bridge bridge,
+				      bool *tx_forward_offload,
+				      struct netlink_ext_ack *extack)
+{
+	struct realtek_priv *priv = ds->priv;
+	u32 mask = 0;
+	int ret;
+	int i;
+
+	/* Add this port to the isolation group of every other port
+	 * offloading this bridge.
+	 */
+	for (i = 0; i < priv->num_ports; i++) {
+		/* Handle this port after */
+		if (i == port)
+			continue;
+
+		/* Skip ports that are not in this bridge */
+		if (!dsa_port_offloads_bridge(dsa_to_port(ds, i), &bridge))
+			continue;
+
+		ret = rtl8365mb_port_add_isolation(priv, i, BIT(port));
+		if (ret)
+			return ret;
+
+		mask |= BIT(i);
+	}
+
+	/* Add those ports to the isolation group of this port */
+	ret = rtl8365mb_port_add_isolation(priv, port, mask);
+	if (ret)
+		return ret;
+
+	/* Use the bridge number as the EFID for this port */
+	ret = rtl8365mb_port_set_efid(priv, port, bridge.num);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void rtl8365mb_port_bridge_leave(struct dsa_switch *ds, int port,
+					struct dsa_bridge bridge)
+{
+	struct realtek_priv *priv = ds->priv;
+	u32 mask = 0;
+	int i;
+
+	/* Remove this port from the isolation group of every other
+	 * port offloading this bridge.
+	 */
+	for (i = 0; i < priv->num_ports; i++) {
+		/* Handle this port after */
+		if (i == port)
+			continue;
+
+		/* Skip ports that are not in this bridge */
+		if (!dsa_port_offloads_bridge(dsa_to_port(ds, i), &bridge))
+			continue;
+
+		rtl8365mb_port_remove_isolation(priv, i, BIT(port));
+
+		mask |= BIT(i);
+	}
+
+	/* Remove those ports from the isolation group of this port */
+	rtl8365mb_port_remove_isolation(priv, port, mask);
+
+	/* Revert to the default EFID 0 for standalone mode */
+	rtl8365mb_port_set_efid(priv, port, 0);
 }
 
 static int rtl8365mb_mib_counter_read(struct realtek_priv *priv, int port,
@@ -2253,6 +2369,9 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
 	if (ret)
 		goto out_teardown_irq;
 
+	/* The EFID is 3 bits, but EFID 0 is reserved for standalone ports */
+	ds->max_num_bridges = FIELD_MAX(RTL8365MB_EFID_MASK);
+
 	/* vlan config will only be effective for ports with vlan filtering */
 	ds->configure_vlan_while_not_filtering = true;
 	/* Set up VLAN */
@@ -2376,6 +2495,8 @@ static const struct dsa_switch_ops rtl8365mb_switch_ops = {
 	.setup = rtl8365mb_setup,
 	.teardown = rtl8365mb_teardown,
 	.phylink_get_caps = rtl8365mb_phylink_get_caps,
+	.port_bridge_join = rtl8365mb_port_bridge_join,
+	.port_bridge_leave = rtl8365mb_port_bridge_leave,
 	.port_stp_state_set = rtl8365mb_port_stp_state_set,
 	.port_vlan_add = rtl8365mb_port_vlan_add,
 	.port_vlan_del = rtl8365mb_port_vlan_del,

-- 
2.53.0


^ permalink raw reply related

* [net-next PATCH 06/10] net: dsa: realtek: rtl8365mb: add VLAN support
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
	Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
  Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>

From: Alvin Šipraga <alsi@bang-olufsen.dk>

Realtek RTL8365MB switches (a.k.a. RTL8367C family) use two different
structures for VLANs:

- VLAN4K: A full table with 4096 entries defining port membership and
  tagging.
- VLANMC: A smaller table with 32 entries used primarily for PVID
  assignment.

In this hardware, a port's PVID must point to an index in the VLANMC
table rather than a VID directly. Since the VLANMC table is limited to
32 entries, the driver implements a dynamic allocation scheme to
maximize resource usage:

- VLAN4K is treated by the driver as the source of truth for membership.
- A VLANMC entry is only allocated when a port is configured to use a
  specific VID as its PVID.
- VLANMC entries are deleted when no longer needed as a PVID by any port.

Although VLANMC has a members field, the switch only checks membership
in the VLAN4K table. However, when a corresponding VLAN entry also exists
in VLANMC, this driver keeps both membership configurations in sync.

VLANMC index 0, although a valid entry, is reserved in this driver as a
neutral PVID value for ports not using a specific PVID.

In the subsequent RTL8367D switch family, VLANMC table was
removed and PVID assignment was delegated to a dedicated set of
registers.

All ports start isolated, forwarding exclusively to CPU ports, and
with VLAN transparent, ignoring VLAN membership. Once a member in a
bridge, the port isolation is expanded to include the bridge members.
When that bridge enables VLAN filtering, the VLAN transparent feature is
disabled, letting the switch filter based on VLAN setup.

Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
 drivers/net/dsa/realtek/Makefile         |   1 +
 drivers/net/dsa/realtek/rtl8365mb_main.c | 256 ++++++++++
 drivers/net/dsa/realtek/rtl8365mb_vlan.c | 805 +++++++++++++++++++++++++++++++
 drivers/net/dsa/realtek/rtl8365mb_vlan.h |  30 ++
 4 files changed, 1092 insertions(+)

diff --git a/drivers/net/dsa/realtek/Makefile b/drivers/net/dsa/realtek/Makefile
index 99654c4c5a3d..b7fc4e852fd8 100644
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -18,3 +18,4 @@ endif
 obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
 rtl8365mb-objs := rtl8365mb_main.o \
 		  rtl8365mb_table.o \
+		  rtl8365mb_vlan.o \
diff --git a/drivers/net/dsa/realtek/rtl8365mb_main.c b/drivers/net/dsa/realtek/rtl8365mb_main.c
index 530ff5503543..c604bd744d38 100644
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -104,6 +104,7 @@
 #include "realtek-smi.h"
 #include "realtek-mdio.h"
 #include "rtl83xx.h"
+#include "rtl8365mb_vlan.h"
 
 /* Family-specific data and limits */
 #define RTL8365MB_PHYADDRMAX		7
@@ -292,6 +293,67 @@
 #define   RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
 		(0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
 
+/* Miscellaneous port configuration register, incl. VLAN egress mode */
+#define RTL8365MB_PORT_MISC_CFG_REG_BASE			0x000E
+#define RTL8365MB_PORT_MISC_CFG_REG(_p) \
+		(RTL8365MB_PORT_MISC_CFG_REG_BASE + ((_p) << 5))
+#define   RTL8365MB_PORT_MISC_CFG_SMALL_TAG_IPG_MASK		0x8000
+#define   RTL8365MB_PORT_MISC_CFG_TX_ITFSP_MODE_MASK		0x4000
+#define   RTL8365MB_PORT_MISC_CFG_FLOWCTRL_INDEP_MASK		0x2000
+#define   RTL8365MB_PORT_MISC_CFG_DOT1Q_REMARK_ENABLE_MASK	0x1000
+#define   RTL8365MB_PORT_MISC_CFG_INGRESSBW_FLOWCTRL_MASK	0x0800
+#define   RTL8365MB_PORT_MISC_CFG_INGRESSBW_IFG_MASK		0x0400
+#define   RTL8365MB_PORT_MISC_CFG_RX_SPC_MASK			0x0200
+#define   RTL8365MB_PORT_MISC_CFG_CRC_SKIP_MASK			0x0100
+#define   RTL8365MB_PORT_MISC_CFG_PKTGEN_TX_FIRST_MASK		0x0080
+#define   RTL8365MB_PORT_MISC_CFG_MAC_LOOPBACK_MASK		0x0040
+/* See &rtl8365mb_vlan_egress_mode */
+#define   RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK		0x0030
+#define   RTL8365MB_PORT_MISC_CFG_CONGESTION_SUSTAIN_TIME_MASK	0x000F
+
+/**
+ * enum rtl8365mb_vlan_egress_mode - port VLAN engress mode
+ * @RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL: follow untag mask in VLAN4k table entry
+ * @RTL8365MB_VLAN_EGRESS_MODE_KEEP: the VLAN tag format of egressed packets
+ * will remain the same as their ingressed format, but the priority and VID
+ * fields may be altered
+ * @RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG: always egress with priority tag
+ * @RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP: the VLAN tag format of egressed
+ * packets will remain the same as their ingressed format, and neither the
+ * priority nor VID fields can be altered
+ */
+enum rtl8365mb_vlan_egress_mode {
+	RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL = 0,
+	RTL8365MB_VLAN_EGRESS_MODE_KEEP = 1,
+	RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG = 2,
+	RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP = 3,
+};
+
+/* VLAN control register */
+#define RTL8365MB_VLAN_CTRL_REG			0x07A8
+#define   RTL8365MB_VLAN_CTRL_EN_MASK		0x0001
+
+/* VLAN ingress filter register */
+#define RTL8365MB_VLAN_INGRESS_REG				0x07A9
+#define   RTL8365MB_VLAN_INGRESS_MASK				GENMASK(10, 0)
+#define   RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_OFFSET(_p)	(_p)
+#define   RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(_p)	BIT(_p)
+
+/* VLAN "transparent" setting registers */
+#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE	0x09D0
+#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(_p) \
+		(RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE + (_p))
+
+/* Frame type filtering registers */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE	0x07aa
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port) \
+		(RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE + ((port) >> 3))
+/* required as FIELD_PREP cannot use non-constant masks */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port) \
+		(0x3 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port))
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port) \
+		(((port) & 0x7) << 1)
+
 /* MIB counter value registers */
 #define RTL8365MB_MIB_COUNTER_BASE	0x1000
 #define RTL8365MB_MIB_COUNTER_REG(_x)	(RTL8365MB_MIB_COUNTER_BASE + (_x))
@@ -1196,6 +1258,183 @@ static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
 			   val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
 }
 
+static int rtl8365mb_port_set_transparent(struct realtek_priv *priv,
+					  int igr_port, int egr_port,
+					  bool enable)
+{
+	dev_dbg(priv->dev, "%s transparent VLAN from %d to %d\n",
+		enable ? "Enable" : "Disable", igr_port, egr_port);
+
+	/* "Transparent" between the two ports means that packets forwarded by
+	 * igr_port and egressed on egr_port will not be filtered by the usual
+	 * VLAN membership settings.
+	 */
+	return regmap_update_bits(priv->map,
+			RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(egr_port),
+			BIT(igr_port), enable ? BIT(igr_port) : 0);
+}
+
+static int rtl8365mb_port_set_ingress_filtering(struct realtek_priv *priv,
+						int port, bool enable)
+{
+	/* Ingress filtering enabled: Discard VLAN-tagged frames if the port is
+	 * not a member of the VLAN with which the packet is associated.
+	 * Untagged packets will also be discarded unless the port has a PVID
+	 * programmed. Priority-tagged frames are treated as untagged frames.
+	 *
+	 * Ingress filtering disabled: Accept all tagged and untagged frames.
+	 */
+	return regmap_update_bits(priv->map, RTL8365MB_VLAN_INGRESS_REG,
+			RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port),
+			enable ?
+			RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port) :
+			0);
+}
+
+static int rtl8365mb_port_vlan_filtering(struct dsa_switch *ds, int port,
+					 bool vlan_filtering,
+					 struct netlink_ext_ack *extack)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct dsa_port *dp;
+	int ret;
+
+	dev_dbg(priv->dev, "port %d: %s VLAN filtering\n", port,
+		vlan_filtering ? "enable" : "disable");
+
+	/* (En|dis)able incoming packets filter, i.e. ignore VLAN membership */
+	dsa_switch_for_each_available_port(dp, ds) {
+		/* after considering port isolation, if not filtering
+		 * allow forwarding from port to dp->index ignoring
+		 * VLAN membership.
+		 */
+		ret = rtl8365mb_port_set_transparent(priv, port, dp->index,
+						     !vlan_filtering);
+		if (ret)
+			return ret;
+	}
+
+	/* If the port is not in the member set, the frame will be dropped */
+	return rtl8365mb_port_set_ingress_filtering(priv, port,
+						    vlan_filtering);
+}
+
+static int rtl8365mb_port_vlan_add(struct dsa_switch *ds, int port,
+				   const struct switchdev_obj_port_vlan *vlan,
+				   struct netlink_ext_ack *extack)
+{
+	bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
+	bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+	struct realtek_priv *priv = ds->priv;
+	int ret;
+
+	dev_dbg(priv->dev, "add VLAN %d on port %d, %s, %s\n",
+		vlan->vid, port, untagged ? "untagged" : "tagged",
+		pvid ? "PVID" : "no PVID");
+	/* add port to vlan4k. It knows nothing about PVID */
+	ret = rtl8365mb_vlan_4k_port_add(ds, port, vlan, extack);
+	if (ret)
+		return ret;
+
+	/* VlanMC knows nothing about untagged but it is required for PVID */
+	ret = rtl8365mb_vlan_mc_port_add(ds, port, vlan, extack);
+	if (ret)
+		goto undo_vlan_4k;
+
+	/* Set PVID if needed */
+	if (pvid) {
+		ret = rtl8365mb_vlan_pvid_port_add(ds, port, vlan, extack);
+		if (ret)
+			goto undo_vlan_mc;
+	}
+
+	return 0;
+
+undo_vlan_mc:
+	(void)rtl8365mb_vlan_mc_port_del(ds, port, vlan);
+
+undo_vlan_4k:
+	(void)rtl8365mb_vlan_4k_port_del(ds, port, vlan);
+	return ret;
+}
+
+static int rtl8365mb_port_vlan_del(struct dsa_switch *ds, int port,
+				   const struct switchdev_obj_port_vlan *vlan)
+{
+	bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
+	bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+	struct realtek_priv *priv = ds->priv;
+	int ret1, ret2, ret3;
+
+	dev_dbg(priv->dev, "del VLAN %d on port %d, %s, %s\n",
+		vlan->vid, port, untagged ? "untagged" : "tagged",
+		pvid ? "PVID" : "no PVID");
+
+	ret1 = rtl8365mb_vlan_pvid_port_del(ds, port, vlan);
+	ret2 = rtl8365mb_vlan_mc_port_del(ds, port, vlan);
+	ret3 = rtl8365mb_vlan_4k_port_del(ds, port, vlan);
+
+	return ret1 ?: ret2 ?: ret3;
+}
+
+static int
+rtl8365mb_port_set_vlan_egress_mode(struct realtek_priv *priv, int port,
+				    enum rtl8365mb_vlan_egress_mode mode)
+{
+	u32 val;
+
+	val = FIELD_PREP(RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, mode);
+	return regmap_update_bits(priv->map,
+			RTL8365MB_PORT_MISC_CFG_REG(port),
+			RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, val);
+}
+
+/* VLAN support is always enabled in the switch.
+ *
+ * All ports starts with vlan-unaware state, letting non-bridge port forward
+ * to CPU.
+ *
+ */
+static int rtl8365mb_vlan_setup(struct dsa_switch *ds)
+{
+	struct realtek_priv *priv = ds->priv;
+	enum rtl8365mb_vlan_egress_mode mode;
+	struct dsa_port *dp;
+	int ret;
+
+	dsa_switch_for_each_user_port(dp, ds) {
+		/* Disable vlan-filtering for all ports */
+		ret = rtl8365mb_port_vlan_filtering(ds, dp->index, false, NULL);
+		if (ret) {
+			dev_err(priv->dev,
+				"Failed to disable vlan filtering on port %d\n",
+				dp->index);
+			return ret;
+		}
+
+		/* The switch default is RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP,
+		 * that forwards the packet as it was received. However,
+		 * different untag settings will require the switch to update
+		 * the tag.
+		 */
+		mode = RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL;
+		ret = rtl8365mb_port_set_vlan_egress_mode(priv, dp->index,
+							  mode);
+		if (ret) {
+			dev_err(priv->dev,
+				"Failed to set port %d egress mode\n",
+				dp->index);
+			return ret;
+		}
+	}
+
+	/* VLAN is always enabled. */
+	ret = regmap_update_bits(priv->map, RTL8365MB_VLAN_CTRL_REG,
+				 RTL8365MB_VLAN_CTRL_EN_MASK,
+				 FIELD_PREP(RTL8365MB_VLAN_CTRL_EN_MASK, 1));
+	return ret;
+}
+
 static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
 				       bool enable)
 {
@@ -2014,6 +2253,20 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
 	if (ret)
 		goto out_teardown_irq;
 
+	/* vlan config will only be effective for ports with vlan filtering */
+	ds->configure_vlan_while_not_filtering = true;
+	/* Set up VLAN */
+	ret = rtl8365mb_vlan_setup(ds);
+	if (ret)
+		goto out_teardown_irq;
+
+	/* Set maximum packet length to 1536 bytes */
+	ret = regmap_update_bits(priv->map, RTL8365MB_CFG0_MAX_LEN_REG,
+				 RTL8365MB_CFG0_MAX_LEN_MASK,
+				 FIELD_PREP(RTL8365MB_CFG0_MAX_LEN_MASK, 1536));
+	if (ret)
+		goto out_teardown_irq;
+
 	ret = rtl83xx_setup_user_mdio(ds);
 	if (ret) {
 		dev_err(priv->dev, "could not set up MDIO bus\n");
@@ -2124,6 +2377,9 @@ static const struct dsa_switch_ops rtl8365mb_switch_ops = {
 	.teardown = rtl8365mb_teardown,
 	.phylink_get_caps = rtl8365mb_phylink_get_caps,
 	.port_stp_state_set = rtl8365mb_port_stp_state_set,
+	.port_vlan_add = rtl8365mb_port_vlan_add,
+	.port_vlan_del = rtl8365mb_port_vlan_del,
+	.port_vlan_filtering = rtl8365mb_port_vlan_filtering,
 	.get_strings = rtl8365mb_get_strings,
 	.get_ethtool_stats = rtl8365mb_get_ethtool_stats,
 	.get_sset_count = rtl8365mb_get_sset_count,
diff --git a/drivers/net/dsa/realtek/rtl8365mb_vlan.c b/drivers/net/dsa/realtek/rtl8365mb_vlan.c
new file mode 100644
index 000000000000..1ac36c06dcf7
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.c
@@ -0,0 +1,805 @@
+// SPDX-License-Identifier: GPL-2.0
+/* VLAN configuration interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ *
+ * VLAN configuration takes place in two separate domains of the switch: the
+ * VLAN4k table and the VLAN membership configuration (MC) database. While the
+ * VLAN4k table is exhaustive and can be fully populated with 4096 VLAN
+ * configurations, the same does not hold for the VLAN membership configuration
+ * database, which is limited to 32 entries.
+ *
+ * The switch will normally only use the VLAN4k table when making forwarding
+ * decisions. The VLAN membership configuration database is a vestigial ASIC
+ * design and is only used for a few specific features in the rtl8365mb
+ * family. This means that the limit of 32 entries should not hinder us in
+ * programming a huge number of VLANs into the switch.
+ *
+ * One necessary use of the VLAN membership configuration database is for the
+ * programming of a port-based VLAN ID (PVID). The PVID is programmed on a
+ * per-port basis via register field, which refers to a specific VLAN membership
+ * configuration via an index 0~31. In order to maintain coherent behaviour on a
+ * port with a PVID, it is necessary to keep the VLAN configuration synchronized
+ * between the VLAN4k table and the VLAN membership configuration database.
+ *
+ * Since VLAN membership configs are a scarce resource, it will only be used
+ * when strictly needed (i.e. a VLAN with members using PVID). Otherwise, the
+ * VLAN4k will be enough.
+ *
+ * With some exceptions, the entries in both the VLAN4k table and the VLAN
+ * membership configuration database offer the same configuration options. The
+ * differences are as follows:
+ *
+ * 1. VLAN4k entries can specify whether to use Independent or Shared VLAN
+ *    Learning (IVL or SVL respectively). VLAN membership config entries
+ *    cannot. This underscores the fact that VLAN membership configs are not
+ *    involved in the learning process of the ASIC.
+ *
+ * 2. VLAN membership config entries use an "enhanced VLAN ID" (efid), which has
+ *    a range 0~8191 compared with the standard 0~4095 range of the VLAN4k
+ *    table. This underscores the fact that VLAN membership configs can be used
+ *    to group ports on a layer beyond the standard VLAN configuration, which
+ *    may be useful for ACL rules which specify alternative forwarding
+ *    decisions.
+ *
+ * VLANMC index 0 is reserved as a neutral PVID, used for vlan-unaware ports.
+ *
+ */
+
+#include "rtl8365mb_vlan.h"
+#include "rtl8365mb_table.h"
+#include <linux/if_bridge.h>
+#include <linux/regmap.h>
+
+/* CVLAN (i.e. VLAN4k) table entry layout, u16[3] */
+#define RTL8365MB_CVLAN_ENTRY_SIZE			3 /* 48-bits */
+#define RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK		GENMASK(7, 0)
+#define RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK		GENMASK(15, 8)
+#define RTL8365MB_CVLAN_ENTRY_D1_FID_MASK		GENMASK(3, 0)
+#define RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK		GENMASK(4, 4)
+#define RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK		GENMASK(7, 5)
+#define RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK		GENMASK(8, 8)
+#define RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK		GENMASK(13, 9)
+#define RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK		GENMASK(14, 14)
+/* extends RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK */
+#define RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK		GENMASK(2, 0)
+/* extends RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK */
+#define RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK		GENMASK(5, 3)
+/* extends RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK */
+#define RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK	GENMASK(6, 6)
+
+/* VLAN member configuration registers 0~31, u16[3] */
+#define RTL8365MB_VLAN_MC_BASE				0x0728
+#define RTL8365MB_VLAN_MC_ENTRY_SIZE			4 /* 64-bit */
+#define RTL8365MB_VLAN_MC_REG(index) \
+		(RTL8365MB_VLAN_MC_BASE + \
+		 (RTL8365MB_VLAN_MC_ENTRY_SIZE * (index)))
+#define   RTL8365MB_VLAN_MC_D0_MBR_MASK			GENMASK(10, 0)
+#define   RTL8365MB_VLAN_MC_D1_FID_MASK			GENMASK(3, 0)
+
+#define   RTL8365MB_VLAN_MC_D2_VBPEN_MASK		GENMASK(0, 0)
+#define   RTL8365MB_VLAN_MC_D2_VBPRI_MASK		GENMASK(3, 1)
+#define   RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK		GENMASK(4, 4)
+#define   RTL8365MB_VLAN_MC_D2_METERIDX_MASK		GENMASK(10, 5)
+#define   RTL8365MB_VLAN_MC_D3_EVID_MASK		GENMASK(12, 0)
+
+/* Some limits for VLAN4k/VLAN membership config entries */
+#define RTL8365MB_PRIORITYMAX	7
+#define RTL8365MB_FIDMAX	15
+#define RTL8365MB_METERMAX	63
+#define RTL8365MB_VLAN_MCMAX	31
+
+/* RTL8367S supports 4k vlans (vid<=4095) and 32 enhanced vlans
+ * for VIDs up to 8191
+ */
+#define RTL8365MB_MAX_4K_VID	0x0FFF /* 4095 */
+#define RTL8365MB_MAX_MC_VID	0x1FFF /* 8191 */
+
+ /* Port-based VID registers 0~5 - each one holds an MC index for two ports */
+#define RTL8365MB_VLAN_PVID_CTRL_BASE			0x0700
+#define RTL8365MB_VLAN_PVID_CTRL_REG(_p) \
+		(RTL8365MB_VLAN_PVID_CTRL_BASE + ((_p) >> 1))
+#define   RTL8365MB_VLAN_PVID_CTRL_PORT0_MCIDX_MASK	0x001F
+#define   RTL8365MB_VLAN_PVID_CTRL_PORT1_MCIDX_MASK	0x1F00
+#define   RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p) \
+		(((_p) & 1) << 3)
+#define   RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(_p) \
+		(0x1F << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p))
+
+/* Frame type filtering registers */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE	0x07aa
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port) \
+		(RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE + ((port) >> 3))
+/* required as FIELD_PREP cannot use non-constant masks */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port) \
+		(0x3 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port))
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port) \
+		(((port) & 0x7) << 1)
+
+/**
+ * struct rtl8365mb_vlan4k - VLAN4k table entry
+ * @vid: VLAN ID (0~4095)
+ * @member: port mask of ports in this VLAN
+ * @untag: port mask of ports which untag on egress
+ * @fid: filter ID - only used with SVL (unused)
+ * @priority: priority classification (unused)
+ * @priority_en: enable priority (unused)
+ * @policing_en: enable policing (unused)
+ * @ivl_en: enable IVL instead of default SVL
+ * @meteridx: metering index (unused)
+ *
+ * This structure is used to get/set entries in the VLAN4k table. The
+ * VLAN4k table dictates the VLAN configuration for the switch for the
+ * vast majority of features.
+ */
+struct rtl8365mb_vlan4k {
+	u16 vid;
+	u16 member;
+	u16 untag;
+	u8 fid : 4;
+	u8 priority : 3;
+	u8 priority_en : 1;
+	u8 policing_en : 1;
+	u8 ivl_en : 1;
+	u8 meteridx : 6;
+};
+
+/**
+ * struct rtl8365mb_vlanmc - VLAN membership config
+ * @evid: Enhanced VLAN ID (0~8191)
+ * @member: port mask of ports in this VLAN
+ * @fid: filter ID - only used with SVL (unused)
+ * @priority: priority classification (unused)
+ * @priority_en: enable priority (unused)
+ * @policing_en: enable policing (unused)
+ * @meteridx: metering index (unused)
+ *
+ * This structure is used to get/set entries in the VLAN membership
+ * configuration database. This feature is largely vestigial, but
+ * still needed for at least the following features:
+ *   - PVID configuration
+ *   - ACL configuration
+ *   - selection of VLAN by the CPU tag when VSEL=1, although the switch
+ *     can also select VLAN based on the VLAN tag if VSEL=0
+ *
+ * This is a low-level structure and it is recommended to interface with
+ * the VLAN membership config database via &struct rtl8365mb_vlanmc_entry.
+ */
+struct rtl8365mb_vlanmc {
+	u16 evid;
+	u16 member;
+	u8 fid : 4;
+	u8 priority : 3;
+	u8 priority_en : 1;
+	u8 policing_en : 1;
+	u8 meteridx : 6;
+};
+
+enum rtl8365mb_frame_ingress {
+	RTL8365MB_FRAME_TYPE_ANY_FRAME = 0,
+	RTL8365MB_FRAME_TYPE_TAGGED_ONLY,
+	RTL8365MB_FRAME_TYPE_UNTAGGED_ONLY,
+};
+
+static int rtl8365mb_vlan_4k_read(struct realtek_priv *priv, u16 vid,
+				  struct rtl8365mb_vlan4k *vlan4k)
+{
+	u16 data[RTL8365MB_CVLAN_ENTRY_SIZE];
+	int ret;
+
+	ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
+				    RTL8365MB_TABLE_OP_READ, &vid, 0, 0,
+				    data, ARRAY_SIZE(data));
+	if (ret)
+		return ret;
+
+	/* Unpack table entry */
+	memset(vlan4k, 0, sizeof(*vlan4k));
+	vlan4k->vid = vid;
+	vlan4k->member =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, data[0]) |
+		(FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK, data[2])
+		 << FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK));
+	vlan4k->untag =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, data[0]) |
+		(FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK, data[2])
+		 << FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK));
+	vlan4k->fid = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, data[1]);
+	vlan4k->priority_en =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK, data[1]);
+	vlan4k->priority =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK, data[1]);
+	vlan4k->policing_en =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK, data[1]);
+	vlan4k->meteridx =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK, data[1]) |
+		(FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK, data[2])
+		 << FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK));
+	vlan4k->ivl_en =
+		FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK, data[1]);
+
+	return 0;
+}
+
+static int rtl8365mb_vlan_4k_write(struct realtek_priv *priv,
+				   const struct rtl8365mb_vlan4k *vlan4k)
+{
+	u16 data[RTL8365MB_CVLAN_ENTRY_SIZE] = { 0 };
+	u16 val;
+	u16 vid;
+
+	if (vlan4k->fid > RTL8365MB_FIDMAX ||
+	    vlan4k->priority > RTL8365MB_PRIORITYMAX ||
+	    vlan4k->meteridx > RTL8365MB_METERMAX)
+		return -EINVAL;
+
+	/* Pack table entry value */
+	data[0] |=
+		FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, vlan4k->member);
+	data[0] |=
+		FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, vlan4k->untag);
+
+	data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, vlan4k->fid);
+	data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK,
+			      vlan4k->priority_en);
+	data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK,
+			      vlan4k->priority);
+	data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK,
+			      vlan4k->policing_en);
+	data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK,
+			      vlan4k->meteridx);
+	data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK,
+			      vlan4k->ivl_en);
+
+	data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK,
+			      vlan4k->member >>
+			      FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK));
+	data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK,
+			      vlan4k->untag >>
+			      FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK));
+	val = FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK,
+			 vlan4k->meteridx >>
+			 FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK));
+	data[2] |= val;
+
+	vid = vlan4k->vid;
+	return rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
+				     RTL8365MB_TABLE_OP_WRITE, &vid, 0, 0,
+				     data, ARRAY_SIZE(data));
+}
+
+#define RTL_VLAN_ERR(msg)						\
+	do {								\
+		const char *__msg = (msg);				\
+									\
+		if (extack)						\
+			NL_SET_ERR_MSG_FMT_MOD(extack, "%s", __msg);	\
+		dev_err(priv->dev, "%s", __msg);			\
+	} while (0)
+
+static int
+rtl8365mb_vlan_4k_port_set(struct dsa_switch *ds, int port,
+			   const struct switchdev_obj_port_vlan *vlan,
+			   struct netlink_ext_ack *extack,
+			       bool include)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb_vlan4k vlan4k = {0};
+	int ret;
+
+	dev_dbg(priv->dev, "%s VLAN %d 4K on port %d\n",
+		include ? "add" : "del",
+		vlan->vid, port);
+
+	if (vlan->vid > RTL8365MB_MAX_4K_VID) {
+		RTL_VLAN_ERR("VLAN ID greater than "
+			     __stringify(RTL8365MB_MAX_4K_VID));
+		return -EINVAL;
+	}
+
+	ret = rtl8365mb_vlan_4k_read(priv, vlan->vid, &vlan4k);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to read VLAN 4k table");
+		return ret;
+	}
+
+	if (include)
+		vlan4k.member |= BIT(port);
+	else
+		vlan4k.member &= ~BIT(port);
+
+	if (include && (vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED))
+		vlan4k.untag |= BIT(port);
+	else
+		vlan4k.untag &= ~BIT(port);
+	vlan4k.ivl_en = true; /* always use Independent VLAN Learning */
+
+	ret = rtl8365mb_vlan_4k_write(priv, &vlan4k);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to write VLAN 4k table");
+		return ret;
+	}
+
+	return 0;
+}
+
+int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan,
+			       struct netlink_ext_ack *extack)
+{
+	return rtl8365mb_vlan_4k_port_set(ds, port, vlan, extack, true);
+}
+
+int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan)
+{
+	return rtl8365mb_vlan_4k_port_set(ds, port, vlan, NULL, false);
+}
+
+/**
+ * rtl8365mb_vlan_mc_find() - find VLANMC index by VID or the first free index
+ *
+ * @priv: realtek_priv pointer
+ * @vid: VLAN ID
+ * @index: found index
+ * @first_free: found free index
+ *
+ * If a VLAN MC entry using @vid was found, @index will return the matched index
+ * and @first_free is undefined. If not found, @index will return 0 and
+ * @first_free will return the first found free index in VLAN MC or 0 if the
+ * table is full.
+ *
+ * Although 0 is a valid VLAN MC index, it is reserved for ports without PVID,
+ * including standalone, non-member ports.
+ *
+ * Both @index and @first_free will be in the * 1..@RTL8365MB_VLAN_MCMAX range.
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ *
+ */
+static int rtl8365mb_vlan_mc_find(struct realtek_priv *priv, u16 vid,
+				  u8 *index, u8 *first_free)
+{
+	u32 vlan_entry_d3;
+	u8 vlanmc_idx;
+	u16 evid;
+	int ret;
+
+	if (!index)
+		return -EINVAL;
+	if (!first_free)
+		return -EINVAL;
+	if (!vid)
+		return -EINVAL;
+
+	*index = 0;
+	*first_free = 0;
+
+	/* look for existing entry or an empty one */
+	/* vlanmc index 0 is reserved as a neutral PVID value.
+	 * Non-PVID ports can still reach the CPU via VLAN
+	 * transparent mode.
+	 **/
+	for (vlanmc_idx = 1; vlanmc_idx <= RTL8365MB_VLAN_MCMAX; vlanmc_idx++) {
+		/* just read the 4th word, where the evid is */
+		ret = regmap_read(priv->map,
+				  RTL8365MB_VLAN_MC_REG(vlanmc_idx) + 3,
+				  &vlan_entry_d3);
+		if (ret)
+			return ret;
+
+		evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlan_entry_d3);
+
+		if (evid == vid) {
+			*index = vlanmc_idx;
+			return 0;
+		}
+
+		if (evid == 0x0 && *first_free < 1)
+			*first_free = vlanmc_idx;
+	}
+	return 0;
+}
+
+static int rtl8365mb_vlan_port_set_pvid(struct realtek_priv *priv,
+					int port, u16 vlanmc_idx)
+{
+	int ret;
+	u32 val;
+
+	val = vlanmc_idx << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
+	ret = regmap_update_bits(priv->map,
+				 RTL8365MB_VLAN_PVID_CTRL_REG(port),
+				 RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port),
+				 val);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static int rtl8365mb_vlan_port_get_pvid(struct realtek_priv *priv,
+					int port, u8 *vlanmc_idx)
+{
+	u32 data;
+	int ret;
+
+	ret = regmap_read(priv->map, RTL8365MB_VLAN_PVID_CTRL_REG(port), &data);
+	if (ret)
+		return ret;
+
+	*vlanmc_idx = (data & RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port))
+		      >> RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
+
+	return 0;
+}
+
+/**
+ * rtl8365mb_vlan_mc_pvid_members() - Get a bitmap of vlan PVID members
+ *
+ * @ds: DSA switch
+ * @vlanmc_idx: the index of a VLAN in VLAN MC table
+ * @members: the returned bitmap of members that have PVID status
+ *
+ * This function iterates over DSA ports and creates a bitmap representation of
+ * those ports that have PVID pointing to this VLAN (identified by its table
+ * index and not VID). If you need to get the table index from VID, see
+ * rtl8365mb_vlan_mc_find()
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ **/
+static int rtl8365mb_vlan_mc_pvid_members(struct dsa_switch *ds,
+					  u8 vlanmc_idx, u16 *members)
+{
+	struct realtek_priv *priv = ds->priv;
+	struct dsa_port *dp;
+	u8 _vlanmc_idx;
+	int ret;
+
+	if (!members)
+		return -EINVAL;
+
+	*members = 0;
+
+	dsa_switch_for_each_port(dp, ds) {
+		ret = rtl8365mb_vlan_port_get_pvid(priv, dp->index,
+						   &_vlanmc_idx);
+		if (ret)
+			return ret;
+
+		if (_vlanmc_idx == vlanmc_idx)
+			*members |= BIT(dp->index);
+	}
+
+	return 0;
+}
+
+static int
+rtl8365mb_vlan_port_set_framefilter(struct realtek_priv *priv,
+				    int port,
+				    enum rtl8365mb_frame_ingress accepted_frame)
+{
+	/* Even if ACCEPT_FRAME_TYPE_ANY, the switch will still check if the
+	 * port is a member of vlan PVID
+	 */
+	accepted_frame = accepted_frame
+			 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port);
+
+	return regmap_update_bits(priv->map,
+				  RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port),
+				  RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port),
+				  accepted_frame);
+}
+
+static int rtl8365mb_vlan_mc_read(struct realtek_priv *priv, u32 index,
+				  struct rtl8365mb_vlanmc *vlanmc)
+{
+	u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE];
+	int ret;
+
+	if (index > RTL8365MB_VLAN_MCMAX)
+		return -EINVAL;
+
+	ret = regmap_bulk_read(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
+			       RTL8365MB_VLAN_MC_ENTRY_SIZE);
+	if (ret)
+		return ret;
+
+	vlanmc->member = FIELD_GET(RTL8365MB_VLAN_MC_D0_MBR_MASK, data[0]);
+	vlanmc->fid = FIELD_GET(RTL8365MB_VLAN_MC_D1_FID_MASK, data[1]);
+	vlanmc->priority = FIELD_GET(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, data[2]);
+	vlanmc->evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, data[3]);
+
+	return 0;
+}
+
+int rtl8365mb_vlan_pvid_port_add(struct dsa_switch *ds, int port,
+				 const struct switchdev_obj_port_vlan *vlan,
+				 struct netlink_ext_ack *extack)
+{
+	bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+	enum rtl8365mb_frame_ingress accepted_frame;
+	struct realtek_priv *priv = ds->priv;
+	u8 _unused_first_free_idx;
+	u8 vlanmc_idx;
+	int ret;
+
+	if (!pvid)
+		return 0;
+
+	/* look for existing entry */
+	ret = rtl8365mb_vlan_mc_find(priv, vlan->vid, &vlanmc_idx,
+				     &_unused_first_free_idx);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to find a VLAN MC table index");
+		return ret;
+	}
+
+	if (!vlanmc_idx) {
+		RTL_VLAN_ERR("VLAN should already exist in VLAN MC");
+		return ret;
+	}
+
+	ret = rtl8365mb_vlan_port_set_pvid(priv, port, vlanmc_idx);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to set port PVID");
+		return ret;
+	}
+
+	/* Changing accept frame is what enables PVID (if not enabled before) */
+	accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
+	ret = rtl8365mb_vlan_port_set_framefilter(priv, port, accepted_frame);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to set port frame filter");
+		return ret;
+	}
+
+	return 0;
+}
+
+int rtl8365mb_vlan_pvid_port_del(struct dsa_switch *ds, int port,
+				 const struct switchdev_obj_port_vlan *vlan)
+{
+	enum rtl8365mb_frame_ingress accepted_frame;
+	struct netlink_ext_ack *extack = NULL;
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb_vlanmc vlanmc = {0};
+	u8 vlanmc_idx;
+	int ret;
+
+	ret = rtl8365mb_vlan_port_get_pvid(priv, port, &vlanmc_idx);
+	if (ret)
+		return ret;
+
+	/* Port is not using PVID. Nothing to remove. */
+	if (!vlanmc_idx)
+		return 0;
+
+	ret = rtl8365mb_vlan_mc_read(priv, vlanmc_idx, &vlanmc);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to read VLAN MC table");
+		return ret;
+	}
+
+	/* We are leaving a non PVID vlan, Nothing to remove. */
+	if (vlanmc.evid != vlan->vid)
+		return 0;
+
+	/* Changing accept frame is what really removes PVID */
+	accepted_frame = RTL8365MB_FRAME_TYPE_TAGGED_ONLY;
+	ret = rtl8365mb_vlan_port_set_framefilter(priv, port, accepted_frame);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to set port frame filter");
+		return ret;
+	}
+
+	ret = rtl8365mb_vlan_port_set_pvid(priv, port, 0);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to set port PVID to 0");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int rtl8365mb_vlan_mc_write(struct realtek_priv *priv, u32 index,
+				   const struct rtl8365mb_vlanmc *vlanmc)
+{
+	u16 data[4] = { 0 };
+	int ret;
+
+	if (index > RTL8365MB_VLAN_MCMAX ||
+	    vlanmc->fid > RTL8365MB_FIDMAX ||
+	    vlanmc->priority > RTL8365MB_PRIORITYMAX ||
+	    vlanmc->meteridx > RTL8365MB_METERMAX)
+		return -EINVAL;
+
+	data[0] |= FIELD_PREP(RTL8365MB_VLAN_MC_D0_MBR_MASK, vlanmc->member);
+	data[1] |= FIELD_PREP(RTL8365MB_VLAN_MC_D1_FID_MASK, vlanmc->fid);
+	data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_METERIDX_MASK,
+			      vlanmc->meteridx);
+	data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK,
+			      vlanmc->policing_en);
+	data[2] |=
+		FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, vlanmc->priority);
+	data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPEN_MASK,
+			      vlanmc->priority_en);
+	data[3] |= FIELD_PREP(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlanmc->evid);
+
+	ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
+				RTL8365MB_VLAN_MC_ENTRY_SIZE);
+
+	return ret;
+}
+
+static int rtl8365mb_vlan_mc_erase(struct realtek_priv *priv, u32 index)
+{
+	u16 data[4] = { 0 };
+	int ret;
+
+	if (index > RTL8365MB_VLAN_MCMAX)
+		return -EINVAL;
+
+	ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
+				RTL8365MB_VLAN_MC_ENTRY_SIZE);
+
+	return ret;
+}
+
+/** rtl8365mb_vlanmc_port_set() - include or exclude a port from vlanMC
+ * @ds: dsa switch
+ * @port: the port number
+ * @vlan: the vlan to include/exclude @port
+ * @extack: optional extack to return errors
+ * @include: whether to include or exclude @port
+ *
+ * This function is used to include/exclude ports to the vlanMC table.
+ *
+ * VlanMC stands for VLAN membership config and it is used exclusively for
+ * PVID. If @vlan members are not using PVID, this function will either
+ * remove or not create a new vlanMC entry.
+ *
+ * vlanMC members are kept in sync with vlan4k, although the switch only
+ * checks membership in vlan4k table.
+ *
+ * Port PVID and accepted frame type are updated as well.
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ *
+ */
+static
+int rtl8365mb_vlan_mc_port_set(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan,
+			       struct netlink_ext_ack *extack,
+			       bool include)
+{
+	bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+	struct realtek_priv *priv = ds->priv;
+	struct rtl8365mb_vlan4k vlan4k = {0};
+	struct rtl8365mb_vlanmc vlanmc = {0};
+	u16 pvid_members = 0;
+	u8 first_unused = 0;
+	u8 vlanmc_idx = 0;
+	int ret;
+
+	dev_dbg(priv->dev, "%s VLAN %d MC on port %d\n",
+		include ? "add" : "del",
+		vlan->vid, port);
+
+	if (vlan->vid > RTL8365MB_MAX_MC_VID) {
+		RTL_VLAN_ERR("VLAN ID greater than "
+			     __stringify(RTL8365MB_MAX_MC_VID));
+		return -EINVAL;
+	}
+
+	/* look for existing entry or an empty slot */
+	ret = rtl8365mb_vlan_mc_find(priv, vlan->vid, &vlanmc_idx,
+				     &first_unused);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to find a VLAN MC table index");
+		return ret;
+	}
+
+	if (vlanmc_idx) {
+		ret = rtl8365mb_vlan_mc_read(priv, vlanmc_idx, &vlanmc);
+		if (ret) {
+			RTL_VLAN_ERR("Failed to read VLAN MC table");
+			return ret;
+		}
+	} else if (include) {
+		/* for now, vlan_mc is only required for PVID. Defer allocation
+		 * until at least one port uses PVID.
+		 */
+		if (!pvid) {
+			dev_dbg(priv->dev,
+				"Not creating VlanMC for vlan %d until a port uses PVID (%d does not)\n",
+				vlan->vid, port);
+			return 0;
+		}
+
+		if (!first_unused) {
+			RTL_VLAN_ERR("All VLAN MC entries ("
+				     __stringify(RTL8365MB_VLAN_MCMAX + 1)
+				     ") are in use.");
+			return -E2BIG;
+		}
+
+		/* Retrieve vlan4k members as we might have deferred VlanMC
+		 * before.
+		 */
+		if (vlan->vid <= RTL8365MB_MAX_4K_VID) {
+			ret = rtl8365mb_vlan_4k_read(priv, vlan->vid, &vlan4k);
+			if (ret) {
+				RTL_VLAN_ERR("Failed to read VLAN 4k table");
+				return ret;
+			}
+		}
+
+		vlanmc_idx = first_unused;
+		vlanmc.evid = vlan->vid;
+
+		/* for new vlan_mc, sync current vlan4k members,
+		 * although only vlan4k members matter.
+		 */
+		vlanmc.member |= vlan4k.member;
+	} else /* excluding and VLANMC not found */ {
+		return 0;
+	}
+
+	ret = rtl8365mb_vlan_mc_pvid_members(ds, vlanmc_idx,
+					     &pvid_members);
+	if (ret) {
+		RTL_VLAN_ERR("Failed to read VLANMC PVID members");
+		return ret;
+	}
+	dev_dbg(priv->dev,
+		"VLAN %d (idx: %d) PVID curr members: %08x\n",
+		vlan->vid, vlanmc_idx, pvid_members);
+
+	/* here we either have an existing VLANMC (with PVID members) or the
+	 * added port is using this VLAN as PVID
+	 */
+	if (include) {
+		vlanmc.member |= BIT(port);
+		if (pvid)
+			pvid_members |= BIT(port);
+	} else {
+		vlanmc.member &= ~BIT(port);
+		pvid_members &= ~BIT(port);
+	}
+
+	/* just like we don't need to create a VLAN_MC when there is no port
+	 * using it as PVID, we can erase it when there is no more port using
+	 * it as PVID.
+	 */
+	if (!pvid_members) {
+		dev_dbg(priv->dev,
+			"Clearing VlanMC index %d previously used by VID %d\n",
+			vlanmc_idx, vlan->vid);
+		ret = rtl8365mb_vlan_mc_erase(priv, vlanmc_idx);
+	} else {
+		dev_dbg(priv->dev,
+			"Saving VlanMC index %d with VID %d\n",
+			vlanmc_idx, vlan->vid);
+		ret = rtl8365mb_vlan_mc_write(priv, vlanmc_idx, &vlanmc);
+	}
+	if (ret) {
+		RTL_VLAN_ERR("Failed to write vlan MC entry");
+		return ret;
+	}
+
+	return 0;
+}
+
+int rtl8365mb_vlan_mc_port_add(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan,
+			       struct netlink_ext_ack *extack)
+{
+	return rtl8365mb_vlan_mc_port_set(ds, port, vlan, extack, true);
+}
+
+int rtl8365mb_vlan_mc_port_del(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan)
+{
+	return rtl8365mb_vlan_mc_port_set(ds, port, vlan, NULL, false);
+}
diff --git a/drivers/net/dsa/realtek/rtl8365mb_vlan.h b/drivers/net/dsa/realtek/rtl8365mb_vlan.h
new file mode 100644
index 000000000000..bd0cf4c804c8
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* VLAN configuration interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ *
+ */
+
+#ifndef _REALTEK_RTL8365MB_VLAN_H
+#define _REALTEK_RTL8365MB_VLAN_H
+
+#include <linux/types.h>
+
+#include "realtek.h"
+
+int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan,
+			       struct netlink_ext_ack *extack);
+int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan);
+int rtl8365mb_vlan_mc_port_add(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan,
+			       struct netlink_ext_ack *extack);
+int rtl8365mb_vlan_mc_port_del(struct dsa_switch *ds, int port,
+			       const struct switchdev_obj_port_vlan *vlan);
+int rtl8365mb_vlan_pvid_port_add(struct dsa_switch *ds, int port,
+				 const struct switchdev_obj_port_vlan *vlan,
+				 struct netlink_ext_ack *extack);
+int rtl8365mb_vlan_pvid_port_del(struct dsa_switch *ds, int port,
+				 const struct switchdev_obj_port_vlan *vlan);
+#endif /* _REALTEK_RTL8365MB_VLAN_H */

-- 
2.53.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox