* [syzbot] [wireless?] [usb?] WARNING in __carl9170_tx_process_status
From: syzbot @ 2026-07-04 5:46 UTC (permalink / raw)
To: chunkeey, linux-kernel, linux-usb, linux-wireless, netdev,
syzkaller-bugs
Hello,
syzbot found the following issue on:
HEAD commit: dc59e4fea9d8 Linux 7.2-rc1
git tree: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
console output: https://syzkaller.appspot.com/x/log.txt?x=1792f9fe580000
kernel config: https://syzkaller.appspot.com/x/.config?x=6ec4d592e55f7960
dashboard link: https://syzkaller.appspot.com/bug?extid=381102a7292a374fe8a7
compiler: gcc (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44
syz repro: https://syzkaller.appspot.com/x/repro.syz?x=1125c289580000
C reproducer: https://syzkaller.appspot.com/x/repro.c?x=17049e89580000
Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/8540695a33d7/disk-dc59e4fe.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/e144bb9cdc33/vmlinux-dc59e4fe.xz
kernel image: https://storage.googleapis.com/syzbot-assets/6e3051bf9301/bzImage-dc59e4fe.xz
IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+381102a7292a374fe8a7@syzkaller.appspotmail.com
------------[ cut here ]------------
__lockdep_enabled && !this_cpu_read(hardirqs_enabled)
WARNING: kernel/softirq.c:430 at __local_bh_enable_ip+0xbd/0x120 kernel/softirq.c:430, CPU#0: swapper/0/0
Modules linked in:
CPU: 0 UID: 0 PID: 0 Comm: swapper/0 Not tainted syzkaller #0 PREEMPT(lazy)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 05/09/2026
RIP: 0010:__local_bh_enable_ip+0xbd/0x120 kernel/softirq.c:430
Code: 00 e8 a7 d1 0b 00 e8 62 10 45 00 fb 65 8b 05 7a c3 86 0b 85 c0 74 50 5b 5d c3 cc cc cc cc 65 8b 05 04 03 87 0b 85 c0 75 a0 90 <0f> 0b 90 eb 9a e8 59 12 45 00 eb 9b 48 89 ef e8 0f 0b 19 00 eb a4
RSP: 0018:ffffc90000007aa8 EFLAGS: 00010046
RAX: 0000000000000000 RBX: 0000000000000201 RCX: 1ffffffff15e7114
RDX: 0000000000000001 RSI: 0000000000000201 RDI: ffffffff8446537b
RBP: ffffffff8446537b R08: 0000000000000000 R09: ffffed1023c03a8b
R10: ffff88811e01d45b R11: 0000000000000000 R12: ffff88811c516705
R13: dffffc0000000000 R14: ffff88811e01b220 R15: ffff88811e01d440
FS: 0000000000000000(0000) GS:ffff888268640000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007ffdd9de6dc4 CR3: 00000001197ae000 CR4: 00000000003506f0
Call Trace:
<IRQ>
spin_unlock_bh include/linux/spinlock.h:396 [inline]
carl9170_get_queued_skb drivers/net/wireless/ath/carl9170/tx.c:531 [inline]
__carl9170_tx_process_status+0x13b/0x620 drivers/net/wireless/ath/carl9170/tx.c:668
carl9170_tx_process_status+0xf6/0x230 drivers/net/wireless/ath/carl9170/tx.c:701
carl9170_handle_command_response+0x3c8/0xc50 drivers/net/wireless/ath/carl9170/rx.c:219
carl9170_usb_rx_irq_complete+0xfc/0x1b0 drivers/net/wireless/ath/carl9170/usb.c:307
__usb_hcd_giveback_urb+0x38d/0x610 drivers/usb/core/hcd.c:1657
usb_hcd_giveback_urb+0x3ca/0x4a0 drivers/usb/core/hcd.c:1741
dummy_timer+0xda1/0x36c0 drivers/usb/gadget/udc/dummy_hcd.c:2005
__run_hrtimer kernel/time/hrtimer.c:2032 [inline]
__hrtimer_run_queues+0x462/0x9c0 kernel/time/hrtimer.c:2096
hrtimer_run_softirq+0x17d/0x2c0 kernel/time/hrtimer.c:2113
handle_softirqs+0x1dd/0x990 kernel/softirq.c:622
__do_softirq kernel/softirq.c:656 [inline]
invoke_softirq kernel/softirq.c:496 [inline]
__irq_exit_rcu+0x160/0x210 kernel/softirq.c:735
irq_exit_rcu+0x9/0x30 kernel/softirq.c:752
instr_sysvec_apic_timer_interrupt arch/x86/kernel/apic/apic.c:1062 [inline]
sysvec_apic_timer_interrupt+0x8f/0xb0 arch/x86/kernel/apic/apic.c:1062
</IRQ>
<TASK>
asm_sysvec_apic_timer_interrupt+0x1a/0x20 arch/x86/include/asm/idtentry.h:674
RIP: 0010:pv_native_safe_halt+0xf/0x20 arch/x86/kernel/paravirt.c:64
Code: 90 ac 01 c3 cc cc cc cc 0f 1f 00 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 f3 0f 1e fa 66 90 0f 00 2d 03 8f 0b 00 fb f4 <e9> 3c f6 02 00 66 2e 0f 1f 84 00 00 00 00 00 66 90 90 90 90 90 90
RSP: 0018:ffffffff89407e20 EFLAGS: 00000242
RAX: 0000000000096967 RBX: ffffffff8942c8c0 RCX: ffffffff877af8e5
RDX: 0000000000000000 RSI: ffffffff890edf5a RDI: ffffffff87b15200
RBP: fffffbfff1285918 R08: 0000000000000001 R09: ffffed103eac670d
R10: ffff8881f563386b R11: 0000000000000000 R12: 0000000000000000
R13: 0000000000000000 R14: 1ffffffff1280fc8 R15: dffffc0000000000
arch_safe_halt arch/x86/include/asm/paravirt.h:62 [inline]
default_idle+0x9/0x10 arch/x86/kernel/process.c:768
default_idle_call+0x6c/0xb0 kernel/sched/idle.c:122
cpuidle_idle_call kernel/sched/idle.c:199 [inline]
do_idle+0x3a7/0x5b0 kernel/sched/idle.c:355
cpu_startup_entry+0x4f/0x60 kernel/sched/idle.c:454
rest_init+0x251/0x260 init/main.c:717
start_kernel+0x489/0x490 init/main.c:1175
x86_64_start_reservations+0x24/0x30 arch/x86/kernel/head64.c:310
x86_64_start_kernel+0x12b/0x130 arch/x86/kernel/head64.c:291
common_startup_64+0x13e/0x158
</TASK>
----------------
Code disassembly (best guess):
0: 90 nop
1: ac lods %ds:(%rsi),%al
2: 01 c3 add %eax,%ebx
4: cc int3
5: cc int3
6: cc int3
7: cc int3
8: 0f 1f 00 nopl (%rax)
b: 90 nop
c: 90 nop
d: 90 nop
e: 90 nop
f: 90 nop
10: 90 nop
11: 90 nop
12: 90 nop
13: 90 nop
14: 90 nop
15: 90 nop
16: 90 nop
17: 90 nop
18: 90 nop
19: 90 nop
1a: 90 nop
1b: f3 0f 1e fa endbr64
1f: 66 90 xchg %ax,%ax
21: 0f 00 2d 03 8f 0b 00 verw 0xb8f03(%rip) # 0xb8f2b
28: fb sti
29: f4 hlt
* 2a: e9 3c f6 02 00 jmp 0x2f66b <-- trapping instruction
2f: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
36: 00 00 00
39: 66 90 xchg %ax,%ax
3b: 90 nop
3c: 90 nop
3d: 90 nop
3e: 90 nop
3f: 90 nop
---
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 syzbot to run the reproducer, reply with:
#syz test: git://repo/address.git branch-or-commit-hash
If you attach or paste a git patch, syzbot will apply it before testing.
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: [RFC] xdp: add device context to bpf_xdp_link_attach_failed tracepoint
From: Masashi Honma @ 2026-07-04 5:50 UTC (permalink / raw)
To: Leon Hwang
Cc: netdev, bpf, linux-trace-kernel, ast, daniel, kuba, hawk, andrii,
rostedt, mhiramat, edumazet, pabeni, linux-kernel
In-Reply-To: <29129c40-4010-4862-9b4b-3bafad874568@linux.dev>
Thank you Leon. I think this is a much better direction than my RFC, so I'll
drop my tracepoint proposal and support your approach instead.
Since a user-space dependency on the existing tracepoint would make it hard
for you to retire it, I'll make sure the Cilium PR doesn't rely on the
tracepoint either.
Is there anything I can help with on the new approach?
Regards,
Masashi Honma
2026年6月29日(月) 0:26 Leon Hwang <leon.hwang@linux.dev>:
>
> On 2026/6/28 19:39, Masashi Honma wrote:
> > Hello, I am re-posting this mail because I forget to add [RFC].
> >
> > The bpf_xdp_link_attach_failed tracepoint (added in commit bf4ea1d0b2cb
> > "xdp: Add tracepoint for xdp attaching failure") exposes the netlink
> > extack message produced when attaching an XDP program via BPF_LINK_CREATE
> > fails. This is useful because, unlike the netlink attach path, the
>
> I really appreciate that the XDP tracepoint helped someone.
>
> > bpf_link attach path does not return the extack to userspace -- the caller
> > only gets an errno (e.g. EINVAL/ERANGE).
> >
> > We would like to use this in Cilium [1][2]: when attaching the XDP
> > datapath program fails, surface the kernel's reason (e.g. "single-buffer
> > XDP requires MTU less than ...") in the agent logs instead of an opaque
> > errno, so operators don't have to inspect dmesg on the host.
> >
> > The limitation we hit is that the tracepoint only carries the message
> > string, so a consumer cannot tell which device a failure belongs to.
> > This matters for two reasons:
> >
> > 1. Correlation: with only the message, a consumer cannot reliably
> > attribute a failure to a specific attach, particularly if multiple
> > XDP attaches happen concurrently.
> > 2. Scoping: a consumer watching this tracepoint sees XDP attach
> > failures system-wide and cannot limit them to the devices it
> > manages.
> >
> > At the call site (bpf_xdp_link_attach() in net/core/dev.c) the net_device
> > is in scope, so exposing it looks straightforward:
> >
> > TRACE_EVENT(bpf_xdp_link_attach_failed,
> > TP_PROTO(const char *msg, const struct net_device *dev),
> > TP_ARGS(msg, dev),
> > TP_STRUCT__entry(
> > __string(msg, msg)
> > __field(int, ifindex)
> > ),
> > TP_fast_assign(
> > __assign_str(msg);
> > __entry->ifindex = dev->ifindex;
> > ),
> > TP_printk("ifindex=%d errmsg=%s", __entry->ifindex, __get_str(msg))
> > );
> >
> > - trace_bpf_xdp_link_attach_failed(extack._msg);
> > + trace_bpf_xdp_link_attach_failed(extack._msg, dev);
> >
> > Before sending a formal patch I'd appreciate guidance on a few points:
> >
> > - Should the tracepoint take const struct net_device *dev (consistent
> > with the other tracepoints in this file, and lets TP_printk show the
> > device), or just the ifindex as an int (simpler for raw_tp BPF
> > consumers, which otherwise read dev->ifindex via CO-RE)?
> >
> > - For raw_tp consumers the argument order is effectively ABI: prepending
> > dev would shift the existing msg argument. I've appended dev above to
> > keep msg at args[0]. Is preserving the existing argument position the
> > right call, or is reordering acceptable given how new and rarely
> > consumed this tracepoint is?
> >
>
> Good concerns. I'm not sure about these parts.
>
> > - Is extending the existing tracepoint preferred, or would you rather
> > keep it as-is and expose the device context some other way?
> >
>
> I'm planning to retire this tracepoint. But I think I cannot do it, if
> there's user space application relied on the tracepoint.
>
> I'm planning to add BPF syscall common attributes support for
> BPF_LINK_CREATE, including XDP link. By that way, the kernel will be
> able to back-propagate the 'extack._msg' to user space, when fail to
> create XDP link. Thereafter, the user space library will be able to get
> the error message alongside the errno.
>
> Thanks,
> Leon
>
> > This would be my first XDP/BPF tracepoint change, so any direction is
> > welcome. I'm happy to send a proper patch once the shape is agreed.
> >
> > Regards,
> > Masashi Honma
> >
> > [1] https://github.com/cilium/cilium/issues/40777
> > [2] https://github.com/cilium/cilium/pull/46546
>
^ permalink raw reply
* Re: [PATCH 1/2] af_unix: Do not wait for garbage collector in sendmsg()
From: Nam Cao @ 2026-07-04 6:03 UTC (permalink / raw)
To: Kuniyuki Iwashima
Cc: sashiko-reviews, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, netdev, linux-kernel, linux-rt-devel
In-Reply-To: <CAAVpQUB+LDxRrF66=NKwLAdLN8kt+Hssm_KBc2oc_q6FTJF0VA@mail.gmail.com>
Kuniyuki Iwashima <kuniyu@google.com> writes:
> your patch makes it much easier to abuse.
> UNIX_INFLIGHT_SANE_USER is usually much smaller than
> RLIMIT_NOFILE.
>
> unix_schedule_gc() in sendmsg() is to self-regulate malicious users,
> otherwise GC relies on unrelated AF_UNIX socket's close() and could
> be triggered too late since GC is system-wide.
About the abuse, the scenario where inflight sockets bypass
UNIX_INFLIGHT_SANE_USER and delay GC until an unrelated AF_UNIX socket
closes actually exists today.
For example, the following program creates far more than
UNIX_INFLIGHT_SANE_USER inflight sockets, which persists indefinitely
until another unrelated AF_UNIX close().
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int send_fd(int unix_fd, int fd)
{
struct msghdr msgh;
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(fd))];
memset(&msgh, 0, sizeof(msgh));
memset(buf, 0, sizeof(buf));
msgh.msg_control = buf;
msgh.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msgh);
cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
msgh.msg_controllen = cmsg->cmsg_len;
memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
return sendmsg(unix_fd, &msgh, 0);
}
int main(int argc, char *argv[])
{
int fd[2];
int i;
for (int n = 0; n < 100; ++n) {
if (socketpair(PF_UNIX, SOCK_SEQPACKET, 0, fd) == -1)
goto out_error;
for (i = 0; i < 100; ++i) {
if (send_fd(fd[0], fd[0]) == -1)
goto out_error;
if (send_fd(fd[1], fd[1]) == -1)
goto out_error;
}
}
return 0;
out_error:
fprintf(stderr, "error: %s\n", strerror(errno));
}
To address this properly, we can schedule the GC at task exit. I can
include that patch in my series, if that sounds good to you.
> Previously every sendmsg() had to wait for GC, and now it's only when
> there is a circular reference AND user has too many inflight sockets.
>
> Please fix the root cause; the former condition on your system.
Can you clarify what you mean by fixing the former condition on my
system. Do you mean ensuring that no application creates a circular
reference?
I am afraid we cannot rely on all users and applications to behave. We
do not want a buggy program or a malicious program to harm another
time-critical task.
Nam
^ permalink raw reply
* [PATCH net v3 0/2] octeon_ep, octeon_ep_vf: fix skb frags overflow in the RX path
From: Maoyi Xie @ 2026-07-04 6:15 UTC (permalink / raw)
To: Veerasenareddy Burru, Sathesh Edara
Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Maciej Fijalkowski, netdev, linux-kernel
Both octeon_ep and octeon_ep_vf build an skb for a multi-buffer RX packet
by adding one fragment per buffer_size chunk of a device-reported length.
Neither bounds the count against MAX_SKB_FRAGS. A long packet yields about
18 fragments, one past the default MAX_SKB_FRAGS of 17, so
skb_add_rx_frag() writes past shinfo->frags[].
Each driver now checks the fragment count before it builds the skb and
drops a packet that would not fit.
v3:
- octeon_ep_vf: pull the drop drain into octep_vf_oq_drop_rx().
The overflow drop and the napi_build_skb failure path both use it.
Suggested by Maciej Fijalkowski.
- octeon_ep: add Maciej's Reviewed-by.
v1: https://lore.kernel.org/r/20260701112825.1653044-1-maoyixie.tju@gmail.com
v2: https://lore.kernel.org/r/20260702180518.2013324-1-maoyixie.tju@gmail.com
Maoyi Xie (2):
octeon_ep: fix skb frags overflow in the RX path
octeon_ep_vf: fix skb frags overflow in the RX path
.../net/ethernet/marvell/octeon_ep/octep_rx.c | 9 ++++
.../marvell/octeon_ep_vf/octep_vf_rx.c | 46 ++++++++++++-------
2 files changed, 39 insertions(+), 16 deletions(-)
--
2.34.1
^ permalink raw reply
* [PATCH net v3 1/2] octeon_ep: fix skb frags overflow in the RX path
From: Maoyi Xie @ 2026-07-04 6:15 UTC (permalink / raw)
To: Veerasenareddy Burru, Sathesh Edara
Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Maciej Fijalkowski, netdev, linux-kernel
In-Reply-To: <20260704061511.2350737-1-maoyixie.tju@gmail.com>
__octep_oq_process_rx() builds an skb for a multi-buffer packet by adding
one fragment per buffer_size chunk:
data_len = buff_info->len - oq->max_single_buffer_size;
while (data_len) {
...
skb_add_rx_frag(skb, shinfo->nr_frags, buff_info->page, 0,
buff_info->len, buff_info->len);
...
}
buff_info->len comes from the device response header
(be64_to_cpu(resp_hw->length)). Nothing bounds the fragment count against
MAX_SKB_FRAGS. data_len can be close to 65535. buffer_size defaults to
about 3776 on 4K pages, so a full packet yields about 18 fragments. That
is one more than the default MAX_SKB_FRAGS of 17, so skb_add_rx_frag()
writes past shinfo->frags[].
The fragment count is now checked before build_skb(). A packet that needs
more fragments than the skb can hold is dropped. octep_oq_drop_rx()
consumes its descriptors like the build_skb failure path. The same class
was fixed in other RX paths, including commit 5ffcb7b890f6 ("net: atlantic:
fix fragment overflow handling in RX path") and commit f0813bcd2d9d ("net:
wwan: t7xx: fix potential skb->frags overflow in RX path").
Fixes: 37d79d059606 ("octeon_ep: add Tx/Rx processing and interrupt support")
Co-developed-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Maoyi Xie <maoyixie.tju@gmail.com>
Reviewed-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
drivers/net/ethernet/marvell/octeon_ep/octep_rx.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/drivers/net/ethernet/marvell/octeon_ep/octep_rx.c b/drivers/net/ethernet/marvell/octeon_ep/octep_rx.c
index e6ebc7e44a..bdbed58c7b 100644
--- a/drivers/net/ethernet/marvell/octeon_ep/octep_rx.c
+++ b/drivers/net/ethernet/marvell/octeon_ep/octep_rx.c
@@ -453,6 +453,15 @@ static int __octep_oq_process_rx(struct octep_device *oct,
octep_oq_next_pkt(oq, buff_info, &read_idx, &desc_used);
+ if (buff_info->len > oq->max_single_buffer_size) {
+ u16 data_len = buff_info->len - oq->max_single_buffer_size;
+
+ if (DIV_ROUND_UP(data_len, oq->buffer_size) > MAX_SKB_FRAGS) {
+ octep_oq_drop_rx(oq, buff_info, &read_idx, &desc_used);
+ continue;
+ }
+ }
+
skb = build_skb((void *)resp_hw, PAGE_SIZE);
if (!skb) {
octep_oq_drop_rx(oq, buff_info,
--
2.34.1
^ permalink raw reply related
* [PATCH net v3 2/2] octeon_ep_vf: fix skb frags overflow in the RX path
From: Maoyi Xie @ 2026-07-04 6:15 UTC (permalink / raw)
To: Veerasenareddy Burru, Sathesh Edara
Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Maciej Fijalkowski, netdev, linux-kernel
In-Reply-To: <20260704061511.2350737-1-maoyixie.tju@gmail.com>
__octep_vf_oq_process_rx() has the same unbounded fragment loop as the PF
driver. buff_info->len comes from the device response header, and one
fragment is added per buffer_size chunk with no check against
MAX_SKB_FRAGS. A long packet yields about 18 fragments, one past the
default MAX_SKB_FRAGS of 17, so skb_add_rx_frag() writes past
shinfo->frags[].
The fragment count is now checked before napi_build_skb(). A packet that
needs more fragments than the skb can hold is dropped.
octep_vf_oq_drop_rx() drains its descriptors. The napi_build_skb()
failure path now uses the same helper.
Fixes: 1cd3b407977c ("octeon_ep_vf: add Tx/Rx processing and interrupt support")
Co-developed-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Maoyi Xie <maoyixie.tju@gmail.com>
---
.../marvell/octeon_ep_vf/octep_vf_rx.c | 46 ++++++++++++-------
1 file changed, 30 insertions(+), 16 deletions(-)
diff --git a/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c b/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c
index d982474082..aa77b673ae 100644
--- a/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c
+++ b/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c
@@ -357,6 +357,29 @@ static inline u32 octep_vf_oq_next_idx(struct octep_vf_oq *oq, u32 idx)
return (idx + 1 == oq->max_count) ? 0 : idx + 1;
}
+static void octep_vf_oq_drop_rx(struct octep_vf_oq *oq,
+ struct octep_vf_rx_buffer *buff_info,
+ u32 *read_idx, u32 *desc_used)
+{
+ u16 data_len = buff_info->len - oq->max_single_buffer_size;
+
+ (*desc_used)++;
+ *read_idx = octep_vf_oq_next_idx(oq, *read_idx);
+ while (data_len) {
+ dma_unmap_page(oq->dev, oq->desc_ring[*read_idx].buffer_ptr,
+ PAGE_SIZE, DMA_FROM_DEVICE);
+ buff_info = (struct octep_vf_rx_buffer *)
+ &oq->buff_info[*read_idx];
+ buff_info->page = NULL;
+ if (data_len < oq->buffer_size)
+ data_len = 0;
+ else
+ data_len -= oq->buffer_size;
+ (*desc_used)++;
+ *read_idx = octep_vf_oq_next_idx(oq, *read_idx);
+ }
+}
+
/**
* __octep_vf_oq_process_rx() - Process hardware Rx queue and push to stack.
*
@@ -431,25 +454,16 @@ static int __octep_vf_oq_process_rx(struct octep_vf_device *oct,
struct skb_shared_info *shinfo;
u16 data_len;
+ data_len = buff_info->len - oq->max_single_buffer_size;
+ if (DIV_ROUND_UP(data_len, oq->buffer_size) > MAX_SKB_FRAGS) {
+ octep_vf_oq_drop_rx(oq, buff_info, &read_idx, &desc_used);
+ continue;
+ }
+
skb = napi_build_skb((void *)resp_hw, PAGE_SIZE);
if (!skb) {
oq->stats->alloc_failures++;
- desc_used++;
- read_idx = octep_vf_oq_next_idx(oq, read_idx);
- data_len = buff_info->len - oq->max_single_buffer_size;
- while (data_len) {
- dma_unmap_page(oq->dev, oq->desc_ring[read_idx].buffer_ptr,
- PAGE_SIZE, DMA_FROM_DEVICE);
- buff_info = (struct octep_vf_rx_buffer *)
- &oq->buff_info[read_idx];
- buff_info->page = NULL;
- if (data_len < oq->buffer_size)
- data_len = 0;
- else
- data_len -= oq->buffer_size;
- desc_used++;
- read_idx = octep_vf_oq_next_idx(oq, read_idx);
- }
+ octep_vf_oq_drop_rx(oq, buff_info, &read_idx, &desc_used);
continue;
}
rx_bytes += buff_info->len;
--
2.34.1
^ permalink raw reply related
* [PATCH nf] netfilter: ipset: skip extension destroy on hash resize replay
From: Weiming Shi @ 2026-07-04 6:22 UTC (permalink / raw)
To: Pablo Neira Ayuso, Jozsef Kadlecsik
Cc: netfilter-devel, coreteam, netdev, linux-kernel, Xiang Mei,
Weiming Shi
During a hash set resize, mtype_resize() copies each element into the
new table with memcpy(), so the new-table element shares the old-table
element's comment extension. An xt_SET delete on the old table during
the resize destroys that shared comment via ip_set_ext_destroy() and
queues a replayed delete on h->ad. After the table swap mtype_resize()
replays it with mtype_del() on the new table, whose copy still points at
the freed comment, so ip_set_ext_destroy() frees it a second time:
ODEBUG: activate active (active state 1) object: ... object type: rcu_head
WARNING: CPU: 3 PID: 5311 at lib/debugobjects.c:514 debug_print_object
Call Trace:
<IRQ>
kvfree_call_rcu (kernel/rcu/tree.c:3825)
ip_set_comment_free (net/netfilter/ipset/ip_set_core.c:397)
hash_ip4_del (net/netfilter/ipset/ip_set_hash_gen.h:1098)
hash_ip4_kadt (net/netfilter/ipset/ip_set_hash_ip.c:96)
ip_set_del (net/netfilter/ipset/ip_set_core.c:813)
set_target_v3 (net/netfilter/xt_set.c:412)
ipt_do_table (net/ipv4/netfilter/ip_tables.c:346)
__ip_local_out (net/ipv4/ip_output.c:119)
icmp_push_reply (net/ipv4/icmp.c:397)
__icmp_send (net/ipv4/icmp.c:804)
__udp4_lib_rcv (net/ipv4/udp.c:2521)
ip_local_deliver (net/ipv4/ip_input.c:254)
ip_rcv (net/ipv4/ip_input.c:569)
</IRQ>
The replay passes a NULL ext (the kernel-side delete that queued it
already destroyed the extensions), so skip ip_set_ext_destroy() when ext
is NULL. This also avoids the NULL ext->target dereference that was only
kept safe by the new table's ref being zero.
Reachable from an unprivileged user namespace.
Fixes: f66ee0410b1c ("netfilter: ipset: Fix \"INFO: rcu detected stall in hash_xxx\" reports")
Reported-by: Xiang Mei <xmei5@asu.edu>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
net/netfilter/ipset/ip_set_hash_gen.h | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index 5e4453e9e..bc909ae2d 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -1080,9 +1080,11 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
mtype_del_cidr(set, h,
NCIDR_PUT(DCIDR_GET(d->cidr, j)), j);
#endif
- ip_set_ext_destroy(set, data);
+ /* On a resize replay the extensions were already destroyed. */
+ if (ext)
+ ip_set_ext_destroy(set, data);
- if (atomic_read(&t->ref) && ext->target) {
+ if (ext && atomic_read(&t->ref) && ext->target) {
/* Resize is in process and kernel side del,
* save values
*/
--
2.43.0
^ permalink raw reply related
* [PATCH v4] r8169: migrate Rx path to page_pool, prepare for XDP
From: Atharva Potdar @ 2026-07-04 8:06 UTC (permalink / raw)
To: Heiner Kallweit, nic_swsd, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni
Cc: Francois Romieu, netdev, atharvapotdar07
Migrate the Rx path to the page_pool API. On MACs newer than
RTL_GIGA_MAC_VER_06, this replaces the alloc_pages() +
skb_copy_to_linear_data() model with napi_build_skb(), giving
zero-copy Rx delivery and laying the groundwork for XDP support.
These MACs are initialized with XDP_PACKET_HEADROOM reserved at the
start of each buffer. This space is needed for skb_push() during
bridged or routed workloads; without it, such workloads fall back to
pskb_expand_head() reallocations on every packet.
MACs up to and including RTL_GIGA_MAC_VER_06 (the original PCI
RTL8169/RTL8110 generation) are kept on the existing copy-based path,
with headroom set to 0. This is a conservative choice rather than a
response to a known, version-specific erratum: this driver's oldest
supported silicon predates the DMA-mapped shared-page model that
zero-copy Rx relies on, and there is comparatively little test
coverage for it.
The pool is locked to order-2 (SZ_16K) allocations on all MACs,
matching the existing R8169_RX_BUF_SIZE, so this migration introduces
no change in per-descriptor allocation size or jumbo-frame behavior
relative to the current tree.
The Rx consumption loop (cur_rx) is decoupled from the buffer refill
loop (dirty_rx). A page is only replaced once napi_build_skb() has
taken ownership of it; under allocation failure, or on the copy path
where the page is reused as-is, the descriptor is left in place for
the NIC to reuse on the next pass. This avoids Tx watchdog timeouts
under memory pressure that the previous unconditional-realloc model
was prone to.
DMA mapping and CPU/device cache synchronization are handled by
page_pool via PP_FLAG_DMA_MAP and PP_FLAG_DMA_SYNC_DEV. The explicit
dma_sync_single_for_device() calls on the copy and failed-build_skb
paths are still required: those pages are returned to the ring
directly rather than through page_pool_put_page(), so the pool's
automatic put-time sync is never invoked for them.
Signed-off-by: Atharva Potdar <atharvapotdar07@gmail.com>
---
Testing:
- Rx path: Verified napi_build_skb() utilization via eBPF. 0 fallback allocations observed at 1Gbps line rate.
- XDP headroom: Confirmed 256B reservation via bpf_xdp_adjust_head(-14). No pskb_expand_head() calls triggered during routing.
- Error path: Injected order-2 allocation failures in the refill loop. Dropped packets as expected without triggering Tx watchdog timeouts.
- Tx ring: Toggled TSO/GSO/SG via ethtool under bidirectional TCP load. No ring stalls or watchdog timeouts.
- skmem: Ran 9k jumbo frame TCP streams. nstat showed no TcpExtTCPRcvQDrop or OfoPrune regressions.
v4:
- Bifurcated headroom by MAC version (0 for legacy, 256 for modern).
Older MACs (<= RTL_GIGA_MAC_VER_06) are kept on the copy path as a
conservative choice given limited test coverage on that hardware,
not in response to a known version-specific erratum.
- Locked pool to order-2 allocations to prevent memory fragmentation
and Tx watchdog panics caused by order-3 churn.
- Fixed memory leak in error path by retaining pages for hardware reuse.
- Fixed prefetch offset to align dynamically with payload.
v3:
- Added XDP_PACKET_HEADROOM to prevent pskb_expand_head() routing overhead.
- Decoupled Rx consumption (cur_rx) from buffer refill (dirty_rx).
- Registered xdp_rxq_info mem model at pool creation time.
v2:
- Reverted buffer size to SZ_16K to prevent MTU regression.
- Replaced skb_add_rx_frag() with napi_build_skb().
v1:
- Initial page_pool migration.
drivers/net/ethernet/realtek/r8169_main.c | 141 ++++++++++++++++++----
1 file changed, 115 insertions(+), 26 deletions(-)
diff --git a/drivers/net/ethernet/realtek/r8169_main.c b/drivers/net/ethernet/realtek/r8169_main.c
index ec4fc21fa..5b13880c7 100644
--- a/drivers/net/ethernet/realtek/r8169_main.c
+++ b/drivers/net/ethernet/realtek/r8169_main.c
@@ -9,6 +9,7 @@
* See MAINTAINERS file for support contact information.
*/
+#include <linux/if_link.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/netdevice.h>
@@ -31,7 +32,9 @@
#include <linux/unaligned.h>
#include <net/ip6_checksum.h>
#include <net/netdev_queues.h>
+#include <net/page_pool/helpers.h>
#include <net/phy/realtek_phy.h>
+#include <net/xdp.h>
#include "r8169.h"
#include "r8169_firmware.h"
@@ -729,6 +732,9 @@ enum rtl_dash_type {
};
struct rtl8169_private {
+ struct page_pool *rx_pool;
+ struct xdp_rxq_info xdp_rxq;
+ u32 rx_headroom;
void __iomem *mmio_addr; /* memory map physical address */
struct pci_dev *pci_dev;
struct net_device *dev;
@@ -739,6 +745,7 @@ struct rtl8169_private {
u32 cur_rx; /* Index into the Rx descriptor buffer of next Rx pkt. */
u32 cur_tx; /* Index into the Tx descriptor buffer of next Rx pkt. */
u32 dirty_tx;
+ u32 dirty_rx;
struct TxDesc *TxDescArray; /* 256-aligned Tx descriptor ring */
struct RxDesc *RxDescArray; /* 256-aligned Rx descriptor ring */
dma_addr_t TxPhyAddr;
@@ -2622,6 +2629,7 @@ static void rtl_init_rxcfg(struct rtl8169_private *tp)
static void rtl8169_init_ring_indexes(struct rtl8169_private *tp)
{
tp->dirty_tx = tp->cur_tx = tp->cur_rx = 0;
+ tp->dirty_rx = 0;
}
static void rtl_jumbo_config(struct rtl8169_private *tp)
@@ -4161,21 +4169,14 @@ static void rtl8169_mark_to_asic(struct RxDesc *desc)
static struct page *rtl8169_alloc_rx_data(struct rtl8169_private *tp,
struct RxDesc *desc)
{
- struct device *d = tp_to_dev(tp);
- int node = dev_to_node(d);
dma_addr_t mapping;
struct page *data;
- data = alloc_pages_node(node, GFP_KERNEL, get_order(R8169_RX_BUF_SIZE));
+ data = page_pool_dev_alloc_pages(tp->rx_pool);
if (!data)
return NULL;
- mapping = dma_map_page(d, data, 0, R8169_RX_BUF_SIZE, DMA_FROM_DEVICE);
- if (unlikely(dma_mapping_error(d, mapping))) {
- netdev_err(tp->dev, "Failed to map RX DMA!\n");
- __free_pages(data, get_order(R8169_RX_BUF_SIZE));
- return NULL;
- }
+ mapping = page_pool_get_dma_addr(data) + tp->rx_headroom;
desc->addr = cpu_to_le64(mapping);
rtl8169_mark_to_asic(desc);
@@ -4188,14 +4189,18 @@ static void rtl8169_rx_clear(struct rtl8169_private *tp)
int i;
for (i = 0; i < NUM_RX_DESC && tp->Rx_databuff[i]; i++) {
- dma_unmap_page(tp_to_dev(tp),
- le64_to_cpu(tp->RxDescArray[i].addr),
- R8169_RX_BUF_SIZE, DMA_FROM_DEVICE);
- __free_pages(tp->Rx_databuff[i], get_order(R8169_RX_BUF_SIZE));
+ page_pool_put_full_page(tp->rx_pool, tp->Rx_databuff[i], false);
tp->Rx_databuff[i] = NULL;
tp->RxDescArray[i].addr = 0;
tp->RxDescArray[i].opts1 = 0;
}
+
+ if (tp->rx_pool) {
+ if (xdp_rxq_info_is_reg(&tp->xdp_rxq))
+ xdp_rxq_info_unreg(&tp->xdp_rxq);
+ page_pool_destroy(tp->rx_pool);
+ tp->rx_pool = NULL;
+ }
}
static int rtl8169_rx_fill(struct rtl8169_private *tp)
@@ -4221,12 +4226,52 @@ static int rtl8169_rx_fill(struct rtl8169_private *tp)
static int rtl8169_init_ring(struct rtl8169_private *tp)
{
+ struct page_pool_params params = {0};
+ int err;
+
rtl8169_init_ring_indexes(tp);
+ if (tp->mac_version <= RTL_GIGA_MAC_VER_06)
+ tp->rx_headroom = 0;
+ else
+ tp->rx_headroom = XDP_PACKET_HEADROOM;
+
+ params.order = get_order(R8169_RX_BUF_SIZE);
+ params.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV;
+ params.pool_size = NUM_RX_DESC;
+ params.nid = dev_to_node(tp_to_dev(tp));
+ params.dev = tp_to_dev(tp);
+ params.napi = &tp->napi;
+ params.dma_dir = DMA_FROM_DEVICE;
+ params.offset = tp->rx_headroom;
+ params.max_len = R8169_RX_BUF_SIZE - tp->rx_headroom;
+
+ tp->rx_pool = page_pool_create(¶ms);
+ if (IS_ERR(tp->rx_pool)) {
+ err = PTR_ERR(tp->rx_pool);
+ tp->rx_pool = NULL;
+ return err;
+ }
+
+ err = xdp_rxq_info_reg(&tp->xdp_rxq, tp->dev, 0, tp->napi.napi_id);
+ if (err)
+ goto err_free_pool;
+
+ err = xdp_rxq_info_reg_mem_model(&tp->xdp_rxq, MEM_TYPE_PAGE_POOL, tp->rx_pool);
+ if (err)
+ goto err_unreg_rxq;
+
memset(tp->tx_skb, 0, sizeof(tp->tx_skb));
memset(tp->Rx_databuff, 0, sizeof(tp->Rx_databuff));
return rtl8169_rx_fill(tp);
+
+err_unreg_rxq:
+ xdp_rxq_info_unreg(&tp->xdp_rxq);
+err_free_pool:
+ page_pool_destroy(tp->rx_pool);
+ tp->rx_pool = NULL;
+ return err;
}
static void rtl8169_unmap_tx_skb(struct rtl8169_private *tp, unsigned int entry)
@@ -4768,16 +4813,39 @@ static inline void rtl8169_rx_csum(struct sk_buff *skb, u32 opts1)
skb_checksum_none_assert(skb);
}
+static void rtl8169_rx_refill(struct rtl8169_private *tp)
+{
+ while (tp->dirty_rx != tp->cur_rx) {
+ unsigned int entry = tp->dirty_rx % NUM_RX_DESC;
+ struct RxDesc *desc = tp->RxDescArray + entry;
+
+ if (!tp->Rx_databuff[entry]) {
+ struct page *new_page = page_pool_dev_alloc_pages(tp->rx_pool);
+
+ if (unlikely(!new_page))
+ break;
+
+ tp->Rx_databuff[entry] = new_page;
+
+ desc->addr = cpu_to_le64(page_pool_get_dma_addr(new_page) +
+ tp->rx_headroom);
+ }
+ rtl8169_mark_to_asic(desc);
+
+ tp->dirty_rx++;
+ }
+}
+
static int rtl_rx(struct net_device *dev, struct rtl8169_private *tp, int budget)
{
struct device *d = tp_to_dev(tp);
int count;
- for (count = 0; count < budget; count++, tp->cur_rx++) {
+ for (count = 0; count < budget;) {
unsigned int pkt_size, entry = tp->cur_rx % NUM_RX_DESC;
struct RxDesc *desc = tp->RxDescArray + entry;
struct sk_buff *skb;
- const void *rx_buf;
+ void *rx_buf;
dma_addr_t addr;
u32 status;
@@ -4820,21 +4888,39 @@ static int rtl_rx(struct net_device *dev, struct rtl8169_private *tp, int budget
goto release_descriptor;
}
- skb = napi_alloc_skb(&tp->napi, pkt_size);
- if (unlikely(!skb)) {
- dev->stats.rx_dropped++;
- goto release_descriptor;
- }
+ if (unlikely(!tp->Rx_databuff[entry]))
+ break;
addr = le64_to_cpu(desc->addr);
rx_buf = page_address(tp->Rx_databuff[entry]);
dma_sync_single_for_cpu(d, addr, pkt_size, DMA_FROM_DEVICE);
- prefetch(rx_buf);
- skb_copy_to_linear_data(skb, rx_buf, pkt_size);
- skb->tail += pkt_size;
- skb->len = pkt_size;
- dma_sync_single_for_device(d, addr, pkt_size, DMA_FROM_DEVICE);
+ prefetch(rx_buf + tp->rx_headroom);
+
+ if (unlikely(tp->rx_headroom == 0)) {
+ skb = napi_alloc_skb(&tp->napi, pkt_size);
+ if (likely(skb)) {
+ skb_copy_to_linear_data(skb, rx_buf, pkt_size);
+ skb_put(skb, pkt_size);
+ }
+ dma_sync_single_for_device(d, addr, pkt_size, DMA_FROM_DEVICE);
+ } else {
+ skb = napi_build_skb(rx_buf, R8169_RX_BUF_SIZE);
+ if (likely(skb)) {
+ skb_reserve(skb, tp->rx_headroom);
+ skb_put(skb, pkt_size);
+ skb_mark_for_recycle(skb);
+
+ tp->Rx_databuff[entry] = NULL;
+ } else {
+ dma_sync_single_for_device(d, addr, pkt_size, DMA_FROM_DEVICE);
+ }
+ }
+
+ if (unlikely(!skb)) {
+ dev->stats.rx_dropped++;
+ goto release_descriptor;
+ }
rtl8169_rx_csum(skb, status);
skb->protocol = eth_type_trans(skb, dev);
@@ -4848,9 +4934,12 @@ static int rtl_rx(struct net_device *dev, struct rtl8169_private *tp, int budget
dev_sw_netstats_rx_add(dev, pkt_size);
release_descriptor:
- rtl8169_mark_to_asic(desc);
+ tp->cur_rx++;
+ count++;
}
+ rtl8169_rx_refill(tp);
+
return count;
}
--
2.55.0
^ permalink raw reply related
* Re: [PATCH 1/1] net: usb: aqc111: fix set_mac_address return value for bonding
From: Hanson Wang @ 2026-07-04 8:42 UTC (permalink / raw)
To: netdev; +Cc: andrew, linux-usb, oneukum
In-Reply-To: <43a0904c-de59-4b40-b186-edd130f6c0cb@lunn.ch>
Hi Andrew,
Thanks for the review. You are right that this is not bonding-specific -
any caller of ndo_set_mac_address() (including netif_set_mac_address()
and "ip link set address") expects 0 on success and treats any non-zero
return value as failure.
I audited all drivers under drivers/net/usb/ that implement a custom
ndo_set_mac_address callback:
aqc111.c - BUG: returns usb_control_msg byte count (6) [this patch]
ax88179_178a.c - OK: returns 0 after ax_write_cmd()
asix_common.c - OK: returns 0 (async write to hardware)
ch397.c - OK: returns 0
dm9601.c - OK: returns 0 (async write)
lan78xx.c - OK: returns 0
mcs7830.c - OK: returns 0
qmi_wwan.c - OK: returns 0
r8152.c/r8157.c - OK: returns 0 on success (via usb_autopm_get_interface)
rtl8150.c - OK: returns 0
sr9700.c - OK: returns 0 (async write)
sr9800.c - OK: returns 0 (async write)
Drivers that use eth_mac_addr as ndo_set_mac_address (smsc95xx,
smsc75xx, cdc_ncm, rndis, pegasus, usbnet default, etc.) always
return 0 from the netdev op.
As far as I can tell, aqc111 is the only driver that directly returns
the result of a USB control write without normalizing success to 0.
Please let me know if you would like any further changes to this patch.
Best regards,
Hanson Wang
^ permalink raw reply
* [PATCH bpf-next v6 0/3] bpf: bidirectional VLAN support for bpf_fib_lookup()
From: Avinash Duduskar @ 2026-07-04 9:21 UTC (permalink / raw)
To: ast, daniel, andrii
Cc: eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil,
john.fastabend, sdf, davem, edumazet, kuba, pabeni, horms, shuah,
hawk, yatsenko, leon.hwang, kpsingh, a.s.protopopov, ameryhung,
rongtao, eyal.birger, bpf, netdev, linux-kernel, linux-kselftest,
toke, dsahern
This series adds VLAN awareness to bpf_fib_lookup() in both directions.
BPF_FIB_LOOKUP_VLAN resolves a VLAN egress to its underlying real device
plus the VLAN tag (XDP programs need this because VLAN devices have no
XDP xmit), and BPF_FIB_LOOKUP_VLAN_INPUT runs the lookup as if a tagged
frame had arrived on the matching VLAN subinterface, for iif policy
routing and VRF table selection.
BPF_FIB_LOOKUP_VLAN opts in to replacing params->ifindex, whose value
existing programs consume since d1c362e1dd68 ("bpf: Always return
target ifindex in bpf_fib_lookup"); without it the output is unchanged.
An egress that does not reduce to a real device plus one tag (a QinQ
stack, or a parent in another network namespace) returns
BPF_FIB_LKUP_RET_VLAN_FAILURE with params->ifindex left at the input;
re-issue without the flag for the VLAN device's own ifindex. A VLAN on
a bond reduces to the bond, which picks its egress slave at xmit.
The new return code is appended after BPF_FIB_LKUP_RET_NO_SRC_ADDR
(nothing renumbered, tools/ mirror updated) and is returned only when
the flag is set, so no existing caller can observe it.
Changes v5 -> v6:
- Patch 1 (BPF_FIB_LOOKUP_VLAN: resolve a VLAN egress to its real
device plus the tag): no code change.
- Patch 2 (BPF_FIB_LOOKUP_VLAN_INPUT: run the lookup as if the tagged
frame arrived on the matching VLAN subinterface): no code change.
- Patch 3 (selftests for both flags, tc and XDP paths): comments
restyled; dead skb.ifindex write removed.
v5: https://lore.kernel.org/all/20260624030530.3342884-1-avinash.duduskar@gmail.com/
v4: https://lore.kernel.org/all/20260623025147.1001664-1-avinash.duduskar@gmail.com/
v3: https://lore.kernel.org/all/20260617224729.1428662-1-avinash.duduskar@gmail.com/
v2: https://lore.kernel.org/all/20260616223426.3568080-1-avinash.duduskar@gmail.com/
v1: https://lore.kernel.org/all/20260609172052.81613-1-avinash.duduskar@gmail.com/
Avinash Duduskar (3):
bpf: Add BPF_FIB_LOOKUP_VLAN flag to bpf_fib_lookup() helper
bpf: Add BPF_FIB_LOOKUP_VLAN_INPUT flag to bpf_fib_lookup() helper
selftests/bpf: Add bpf_fib_lookup() VLAN flag tests
include/uapi/linux/bpf.h | 50 +-
net/core/filter.c | 97 ++-
tools/include/uapi/linux/bpf.h | 50 +-
.../selftests/bpf/prog_tests/fib_lookup.c | 712 +++++++++++++++++-
.../testing/selftests/bpf/progs/fib_lookup.c | 36 +
5 files changed, 931 insertions(+), 14 deletions(-)
base-commit: a975094bf98ca97be9146f9d3b5681a6f9cf5ce3
--
2.54.0
^ permalink raw reply
* [PATCH bpf-next v6 1/3] bpf: Add BPF_FIB_LOOKUP_VLAN flag to bpf_fib_lookup() helper
From: Avinash Duduskar @ 2026-07-04 9:21 UTC (permalink / raw)
To: ast, daniel, andrii
Cc: eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil,
john.fastabend, sdf, davem, edumazet, kuba, pabeni, horms, shuah,
hawk, yatsenko, leon.hwang, kpsingh, a.s.protopopov, ameryhung,
rongtao, eyal.birger, bpf, netdev, linux-kernel, linux-kselftest,
toke, dsahern
In-Reply-To: <20260704092159.1256823-1-avinash.duduskar@gmail.com>
bpf_fib_lookup() returns the FIB-resolved egress ifindex straight
from the fib result. When the egress is a VLAN device, the returned
ifindex is the VLAN netdev's, which has no XDP xmit handler; XDP
programs that want to forward the frame (e.g. xdp-forward) must
instead target the underlying physical device and push the VLAN tag
themselves. Today the program has no way to learn either the
underlying ifindex or the VLAN tag without maintaining its own
VLAN-to-ifindex map in userspace and refreshing it on netlink
events.
Add BPF_FIB_LOOKUP_VLAN. When the caller sets this flag and the fib
result is a VLAN device whose immediate parent is a real (non-VLAN)
device in the same network namespace, populate the existing output
fields params->h_vlan_proto and params->h_vlan_TCI from the VLAN
device and replace params->ifindex with the parent's ifindex.
params->h_vlan_TCI carries the VID only, with PCP and DEI bits zero; a
consumer wanting to set egress priority writes PCP itself.
params->smac is the VLAN device's own address, which can differ from
the parent's.
Only the immediate parent is resolved, via vlan_dev_priv(dev)->real_dev
and not vlan_dev_real_dev(), which walks to the bottom of a stack. When
the immediate parent is not a real device in the same namespace, the
lookup returns BPF_FIB_LKUP_RET_VLAN_FAILURE and leaves params->ifindex
at the input. This covers a stacked VLAN (QinQ), where the immediate
parent is itself a VLAN device and one h_vlan_proto/h_vlan_TCI pair
cannot describe two tags, and a parent in another network namespace (a
VLAN device can be moved while its parent stays), whose ifindex would
be meaningless in the caller's namespace. A program that wants the VLAN
device's own ifindex re-issues the lookup without BPF_FIB_LOOKUP_VLAN,
so the unreducible case stays distinct from a physical egress. That
distinction matters for XDP: a program cannot xmit on a VLAN device, so
a success carrying the VLAN ifindex would make it redirect to a device
with no ndo_xdp_xmit and drop the frame at xdp_do_flush(). The swap and
the vlan fields are written only on the reduce path; other output
fields keep their existing behaviour, so a frag-needed result still
reports the route mtu in params->mtu_result.
BPF_FIB_LOOKUP_VLAN is only useful to XDP, which cannot redirect to a
VLAN device. A tc program can redirect to the VLAN device directly, so
bpf_skb_fib_lookup() rejects the flag with -EINVAL; bpf_xdp_fib_lookup()
accepts it. When the flag is not set, behaviour is unchanged:
h_vlan_proto and h_vlan_TCI are zeroed and ifindex is left at the FIB
result.
The new block is compiled only under CONFIG_VLAN_8021Q since
vlan_dev_priv() is not defined otherwise; without that config
is_vlan_dev() is constant false and the flag is accepted but never
acts. That is safe because no VLAN device can exist there, so every
egress is already physical.
This lets an XDP redirect target the physical device and learn the
tag to push in a single lookup, which xdp-forward's optional VLAN
mode (xdp-project/xdp-tools#504) wants from the kernel side.
The helper's input semantics are unchanged; the reverse direction
(supplying a tag as lookup input) is added in the following patch.
Suggested-by: Toke Høiland-Jørgensen <toke@redhat.com>
Reviewed-by: Toke Høiland-Jørgensen <toke@redhat.com>
Acked-by: David Ahern <dsahern@kernel.org>
Signed-off-by: Avinash Duduskar <avinash.duduskar@gmail.com>
---
include/uapi/linux/bpf.h | 31 ++++++++++++++++++++++++++++++-
net/core/filter.c | 33 +++++++++++++++++++++++++++++----
tools/include/uapi/linux/bpf.h | 31 ++++++++++++++++++++++++++++++-
3 files changed, 89 insertions(+), 6 deletions(-)
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5fdbb..e00f0392e728 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -3532,6 +3532,29 @@ union bpf_attr {
* Use the mark present in *params*->mark for the fib lookup.
* This option should not be used with BPF_FIB_LOOKUP_DIRECT,
* as it only has meaning for full lookups.
+ * **BPF_FIB_LOOKUP_VLAN**
+ * If the fib lookup resolves to a VLAN device whose
+ * parent is a real (non-VLAN) device, set
+ * *params*->h_vlan_proto and *params*->h_vlan_TCI from
+ * the VLAN device and replace *params*->ifindex with the
+ * parent's ifindex. *params*->h_vlan_TCI carries the VID
+ * only, with PCP and DEI bits zero; a consumer wanting to
+ * set egress priority writes PCP itself. *params*->smac is
+ * the VLAN device's own address, which can differ from the
+ * parent's. Only the immediate parent is resolved; if it
+ * is itself a VLAN device (QinQ) or in another namespace,
+ * the egress cannot be reduced to a physical device plus
+ * one tag and the lookup returns
+ * **BPF_FIB_LKUP_RET_VLAN_FAILURE** with *params*->ifindex
+ * left at the input. Re-issue without
+ * **BPF_FIB_LOOKUP_VLAN** to obtain the VLAN device's own
+ * ifindex. The swap and the vlan fields
+ * are written only on success; other output fields keep
+ * the helper's existing behaviour, so a frag-needed result
+ * still reports the route mtu in *params*->mtu_result.
+ * This flag is only valid for XDP programs; tc programs
+ * receive -EINVAL since they can redirect to the VLAN
+ * device directly.
*
* *ctx* is either **struct xdp_md** for XDP programs or
* **struct sk_buff** tc cls_act programs.
@@ -7327,6 +7350,7 @@ enum {
BPF_FIB_LOOKUP_TBID = (1U << 3),
BPF_FIB_LOOKUP_SRC = (1U << 4),
BPF_FIB_LOOKUP_MARK = (1U << 5),
+ BPF_FIB_LOOKUP_VLAN = (1U << 6),
};
enum {
@@ -7340,6 +7364,7 @@ enum {
BPF_FIB_LKUP_RET_NO_NEIGH, /* no neighbor entry for nh */
BPF_FIB_LKUP_RET_FRAG_NEEDED, /* fragmentation required to fwd */
BPF_FIB_LKUP_RET_NO_SRC_ADDR, /* failed to derive IP src addr */
+ BPF_FIB_LKUP_RET_VLAN_FAILURE, /* VLAN egress, parent unresolvable */
};
struct bpf_fib_lookup {
@@ -7393,7 +7418,11 @@ struct bpf_fib_lookup {
union {
struct {
- /* output */
+ /*
+ * output with BPF_FIB_LOOKUP_VLAN: set from the
+ * resolved egress VLAN device (see the flag); zeroed
+ * on other successful lookups.
+ */
__be16 h_vlan_proto;
__be16 h_vlan_TCI;
};
diff --git a/net/core/filter.c b/net/core/filter.c
index 2e96b4b847ce..b5a45485a54b 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -6201,10 +6201,29 @@ static const struct bpf_func_proto bpf_skb_get_xfrm_state_proto = {
#endif
#if IS_ENABLED(CONFIG_INET) || IS_ENABLED(CONFIG_IPV6)
-static int bpf_fib_set_fwd_params(struct bpf_fib_lookup *params, u32 mtu)
+static int bpf_fib_set_fwd_params(struct net_device *dev,
+ struct bpf_fib_lookup *params,
+ u32 flags, u32 mtu, u32 in_ifindex)
{
params->h_vlan_TCI = 0;
params->h_vlan_proto = 0;
+
+#if IS_ENABLED(CONFIG_VLAN_8021Q)
+ if ((flags & BPF_FIB_LOOKUP_VLAN) && is_vlan_dev(dev)) {
+ struct net_device *real_dev = vlan_dev_priv(dev)->real_dev;
+
+ if (!is_vlan_dev(real_dev) &&
+ net_eq(dev_net(real_dev), dev_net(dev))) {
+ params->h_vlan_proto = vlan_dev_vlan_proto(dev);
+ params->h_vlan_TCI = htons(vlan_dev_vlan_id(dev));
+ params->ifindex = real_dev->ifindex;
+ } else {
+ params->ifindex = in_ifindex;
+ return BPF_FIB_LKUP_RET_VLAN_FAILURE;
+ }
+ }
+#endif
+
if (mtu)
params->mtu_result = mtu; /* union with tot_len */
@@ -6216,6 +6235,7 @@ static int bpf_fib_set_fwd_params(struct bpf_fib_lookup *params, u32 mtu)
static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
u32 flags, bool check_mtu)
{
+ u32 in_ifindex = params->ifindex;
struct neighbour *neigh = NULL;
struct fib_nh_common *nhc;
struct in_device *in_dev;
@@ -6347,7 +6367,7 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
memcpy(params->smac, dev->dev_addr, ETH_ALEN);
set_fwd_params:
- return bpf_fib_set_fwd_params(params, mtu);
+ return bpf_fib_set_fwd_params(dev, params, flags, mtu, in_ifindex);
}
#endif
@@ -6357,6 +6377,7 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
{
struct in6_addr *src = (struct in6_addr *) params->ipv6_src;
struct in6_addr *dst = (struct in6_addr *) params->ipv6_dst;
+ u32 in_ifindex = params->ifindex;
struct fib6_result res = {};
struct neighbour *neigh;
struct net_device *dev;
@@ -6486,13 +6507,14 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
memcpy(params->smac, dev->dev_addr, ETH_ALEN);
set_fwd_params:
- return bpf_fib_set_fwd_params(params, mtu);
+ return bpf_fib_set_fwd_params(dev, params, flags, mtu, in_ifindex);
}
#endif
#define BPF_FIB_LOOKUP_MASK (BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT | \
BPF_FIB_LOOKUP_SKIP_NEIGH | BPF_FIB_LOOKUP_TBID | \
- BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK)
+ BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK | \
+ BPF_FIB_LOOKUP_VLAN)
BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,
struct bpf_fib_lookup *, params, int, plen, u32, flags)
@@ -6541,6 +6563,9 @@ BPF_CALL_4(bpf_skb_fib_lookup, struct sk_buff *, skb,
if (flags & ~BPF_FIB_LOOKUP_MASK)
return -EINVAL;
+ if (flags & BPF_FIB_LOOKUP_VLAN)
+ return -EINVAL;
+
if (params->tot_len)
check_mtu = true;
diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h
index 89b36de5fdbb..e00f0392e728 100644
--- a/tools/include/uapi/linux/bpf.h
+++ b/tools/include/uapi/linux/bpf.h
@@ -3532,6 +3532,29 @@ union bpf_attr {
* Use the mark present in *params*->mark for the fib lookup.
* This option should not be used with BPF_FIB_LOOKUP_DIRECT,
* as it only has meaning for full lookups.
+ * **BPF_FIB_LOOKUP_VLAN**
+ * If the fib lookup resolves to a VLAN device whose
+ * parent is a real (non-VLAN) device, set
+ * *params*->h_vlan_proto and *params*->h_vlan_TCI from
+ * the VLAN device and replace *params*->ifindex with the
+ * parent's ifindex. *params*->h_vlan_TCI carries the VID
+ * only, with PCP and DEI bits zero; a consumer wanting to
+ * set egress priority writes PCP itself. *params*->smac is
+ * the VLAN device's own address, which can differ from the
+ * parent's. Only the immediate parent is resolved; if it
+ * is itself a VLAN device (QinQ) or in another namespace,
+ * the egress cannot be reduced to a physical device plus
+ * one tag and the lookup returns
+ * **BPF_FIB_LKUP_RET_VLAN_FAILURE** with *params*->ifindex
+ * left at the input. Re-issue without
+ * **BPF_FIB_LOOKUP_VLAN** to obtain the VLAN device's own
+ * ifindex. The swap and the vlan fields
+ * are written only on success; other output fields keep
+ * the helper's existing behaviour, so a frag-needed result
+ * still reports the route mtu in *params*->mtu_result.
+ * This flag is only valid for XDP programs; tc programs
+ * receive -EINVAL since they can redirect to the VLAN
+ * device directly.
*
* *ctx* is either **struct xdp_md** for XDP programs or
* **struct sk_buff** tc cls_act programs.
@@ -7327,6 +7350,7 @@ enum {
BPF_FIB_LOOKUP_TBID = (1U << 3),
BPF_FIB_LOOKUP_SRC = (1U << 4),
BPF_FIB_LOOKUP_MARK = (1U << 5),
+ BPF_FIB_LOOKUP_VLAN = (1U << 6),
};
enum {
@@ -7340,6 +7364,7 @@ enum {
BPF_FIB_LKUP_RET_NO_NEIGH, /* no neighbor entry for nh */
BPF_FIB_LKUP_RET_FRAG_NEEDED, /* fragmentation required to fwd */
BPF_FIB_LKUP_RET_NO_SRC_ADDR, /* failed to derive IP src addr */
+ BPF_FIB_LKUP_RET_VLAN_FAILURE, /* VLAN egress, parent unresolvable */
};
struct bpf_fib_lookup {
@@ -7393,7 +7418,11 @@ struct bpf_fib_lookup {
union {
struct {
- /* output */
+ /*
+ * output with BPF_FIB_LOOKUP_VLAN: set from the
+ * resolved egress VLAN device (see the flag); zeroed
+ * on other successful lookups.
+ */
__be16 h_vlan_proto;
__be16 h_vlan_TCI;
};
--
2.54.0
^ permalink raw reply related
* [PATCH bpf-next v6 2/3] bpf: Add BPF_FIB_LOOKUP_VLAN_INPUT flag to bpf_fib_lookup() helper
From: Avinash Duduskar @ 2026-07-04 9:21 UTC (permalink / raw)
To: ast, daniel, andrii
Cc: eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil,
john.fastabend, sdf, davem, edumazet, kuba, pabeni, horms, shuah,
hawk, yatsenko, leon.hwang, kpsingh, a.s.protopopov, ameryhung,
rongtao, eyal.birger, bpf, netdev, linux-kernel, linux-kselftest,
toke, dsahern
In-Reply-To: <20260704092159.1256823-1-avinash.duduskar@gmail.com>
BPF_FIB_LOOKUP_VLAN resolves a VLAN egress. The reverse is also
useful: an XDP program receiving a VLAN-tagged frame on a physical
device wants the lookup to behave as if the packet had arrived on the
corresponding VLAN subinterface, so iif-based policy routing and VRF
table selection use the right ingress.
Add BPF_FIB_LOOKUP_VLAN_INPUT. When set, params->h_vlan_proto and
params->h_vlan_TCI are read as an input VLAN tag and the matching VLAN
device of params->ifindex is resolved with __vlan_find_dev_deep_rcu().
The device must be up and in the same network namespace as
params->ifindex (a VLAN device can be moved to another netns while
registered on its parent; receive would deliver into that other
namespace, which a lookup here cannot represent). If params->ifindex
is itself a VLAN device, its inner (QinQ) subinterface is matched.
For a bond or team, a tag on a port matches no device and returns
NOT_FWDED; pass the master's ifindex.
The lookup then runs with the resolved device as the ingress;
params->ifindex itself is not modified on the input side. When the
resolved device is enslaved to a VRF, both the full lookup (via the
l3mdev rule) and BPF_FIB_LOOKUP_DIRECT (via l3mdev_fib_table_rcu())
select the VRF's table from the resolved ingress. That follows from
feeding the resolved device to the flow as the ingress
(fl4.flowi4_iif = dev->ifindex), which is what makes l3mdev resolve
the VRF master from the subinterface rather than from
params->ifindex.
The two failure classes get different treatment on purpose. A
h_vlan_proto other than 802.1Q/802.1ad is API misuse and returns
-EINVAL, since it would otherwise reach the WARN in vlan_proto_idx()
with a program-controlled value. An unmatched VID, a device that is
down, or one in another namespace is a data outcome and returns
BPF_FIB_LKUP_RET_NOT_FWDED, matching the DIRECT path when
fib_get_table() finds no table and mirroring real ingress, where the
receive path drops such frames. A VID of 0 (a priority tag) is looked
up literally and normally fails the same way; receive instead
processes such frames untagged, so callers should not set the flag for
priority tags. Proceeding on the physical device for any of these
would be fail-open for the policy-routing cases above.
The h_vlan fields share a union with tbid, so the flag cannot be
combined with BPF_FIB_LOOKUP_TBID. It describes ingress, so it also
cannot be combined with BPF_FIB_LOOKUP_OUTPUT. Both combinations
return -EINVAL; restricting now keeps a later relaxation backward
compatible. Combining with BPF_FIB_LOOKUP_VLAN is allowed: the tag is
consumed on the ingress side and the egress tag is written on
success.
Under !CONFIG_VLAN_8021Q the __vlan_find_dev_deep_rcu() stub returns
NULL, so every lookup with the flag returns NOT_FWDED, which is
correct since no VLAN device can exist.
Suggested-by: Toke Høiland-Jørgensen <toke@redhat.com>
Reviewed-by: Toke Høiland-Jørgensen <toke@redhat.com>
Signed-off-by: Avinash Duduskar <avinash.duduskar@gmail.com>
---
include/uapi/linux/bpf.h | 21 ++++++++++-
net/core/filter.c | 66 +++++++++++++++++++++++++++++++---
tools/include/uapi/linux/bpf.h | 21 ++++++++++-
3 files changed, 101 insertions(+), 7 deletions(-)
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index e00f0392e728..d4218954c50f 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -3555,6 +3555,22 @@ union bpf_attr {
* This flag is only valid for XDP programs; tc programs
* receive -EINVAL since they can redirect to the VLAN
* device directly.
+ * **BPF_FIB_LOOKUP_VLAN_INPUT**
+ * Treat *params*->h_vlan_proto and *params*->h_vlan_TCI
+ * as an input VLAN tag and run the lookup as if ingress
+ * had happened on the VLAN subinterface carrying that tag
+ * on *params*->ifindex. The VID is the low 12 bits of
+ * *params*->h_vlan_TCI; *params*->h_vlan_proto must be
+ * ETH_P_8021Q or ETH_P_8021AD in network byte order, else
+ * **-EINVAL**. If *params*->ifindex is itself a VLAN
+ * device, its inner (QinQ) subinterface is matched; for a
+ * bond or team, pass the master's ifindex. An unmatched
+ * tag, a down device, or one in another namespace returns
+ * **BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress.
+ * A VID of 0 is looked up literally, so do not set this
+ * flag for priority-tagged frames. Cannot be combined with
+ * **BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT**
+ * (returns **-EINVAL**).
*
* *ctx* is either **struct xdp_md** for XDP programs or
* **struct sk_buff** tc cls_act programs.
@@ -7351,6 +7367,7 @@ enum {
BPF_FIB_LOOKUP_SRC = (1U << 4),
BPF_FIB_LOOKUP_MARK = (1U << 5),
BPF_FIB_LOOKUP_VLAN = (1U << 6),
+ BPF_FIB_LOOKUP_VLAN_INPUT = (1U << 7),
};
enum {
@@ -7421,7 +7438,9 @@ struct bpf_fib_lookup {
/*
* output with BPF_FIB_LOOKUP_VLAN: set from the
* resolved egress VLAN device (see the flag); zeroed
- * on other successful lookups.
+ * on other successful lookups. input with
+ * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope
+ * the lookup by.
*/
__be16 h_vlan_proto;
__be16 h_vlan_TCI;
diff --git a/net/core/filter.c b/net/core/filter.c
index b5a45485a54b..0ea362fa4287 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -6229,6 +6229,25 @@ static int bpf_fib_set_fwd_params(struct net_device *dev,
return 0;
}
+
+static struct net_device *bpf_fib_vlan_input_dev(struct net_device *dev,
+ const struct bpf_fib_lookup *params)
+{
+ __be16 proto = params->h_vlan_proto;
+ struct net_device *vlan_dev;
+ u16 vid;
+
+ if (proto != htons(ETH_P_8021Q) && proto != htons(ETH_P_8021AD))
+ return ERR_PTR(-EINVAL);
+
+ vid = ntohs(params->h_vlan_TCI) & VLAN_VID_MASK;
+ vlan_dev = __vlan_find_dev_deep_rcu(dev, proto, vid);
+ if (!vlan_dev || !(vlan_dev->flags & IFF_UP) ||
+ !net_eq(dev_net(vlan_dev), dev_net(dev)))
+ return NULL;
+
+ return vlan_dev;
+}
#endif
#if IS_ENABLED(CONFIG_INET)
@@ -6249,6 +6268,14 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
if (unlikely(!dev))
return -ENODEV;
+ if (flags & BPF_FIB_LOOKUP_VLAN_INPUT) {
+ dev = bpf_fib_vlan_input_dev(dev, params);
+ if (IS_ERR(dev))
+ return PTR_ERR(dev);
+ if (!dev)
+ return BPF_FIB_LKUP_RET_NOT_FWDED;
+ }
+
/* verify forwarding is enabled on this interface */
in_dev = __in_dev_get_rcu(dev);
if (unlikely(!in_dev || !IN_DEV_FORWARD(in_dev)))
@@ -6258,7 +6285,11 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
fl4.flowi4_iif = 1;
fl4.flowi4_oif = params->ifindex;
} else {
- fl4.flowi4_iif = params->ifindex;
+ /*
+ * dev->ifindex, not params->ifindex: VLAN_INPUT may have
+ * resolved dev to a subinterface above.
+ */
+ fl4.flowi4_iif = dev->ifindex;
fl4.flowi4_oif = 0;
}
fl4.flowi4_dscp = inet_dsfield_to_dscp(params->tos);
@@ -6395,6 +6426,14 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
if (unlikely(!dev))
return -ENODEV;
+ if (flags & BPF_FIB_LOOKUP_VLAN_INPUT) {
+ dev = bpf_fib_vlan_input_dev(dev, params);
+ if (IS_ERR(dev))
+ return PTR_ERR(dev);
+ if (!dev)
+ return BPF_FIB_LKUP_RET_NOT_FWDED;
+ }
+
idev = __in6_dev_get_safely(dev);
if (unlikely(!idev || !READ_ONCE(idev->cnf.forwarding)))
return BPF_FIB_LKUP_RET_FWD_DISABLED;
@@ -6403,7 +6442,12 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
fl6.flowi6_iif = 1;
oif = fl6.flowi6_oif = params->ifindex;
} else {
- oif = fl6.flowi6_iif = params->ifindex;
+ /*
+ * dev->ifindex, not params->ifindex: VLAN_INPUT may have
+ * resolved dev to a subinterface above.
+ */
+ oif = dev->ifindex;
+ fl6.flowi6_iif = oif;
fl6.flowi6_oif = 0;
strict = RT6_LOOKUP_F_HAS_SADDR;
}
@@ -6514,7 +6558,19 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
#define BPF_FIB_LOOKUP_MASK (BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT | \
BPF_FIB_LOOKUP_SKIP_NEIGH | BPF_FIB_LOOKUP_TBID | \
BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK | \
- BPF_FIB_LOOKUP_VLAN)
+ BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_VLAN_INPUT)
+
+static bool bpf_fib_lookup_flags_ok(u32 flags)
+{
+ if (flags & ~BPF_FIB_LOOKUP_MASK)
+ return false;
+
+ if ((flags & BPF_FIB_LOOKUP_VLAN_INPUT) &&
+ (flags & (BPF_FIB_LOOKUP_TBID | BPF_FIB_LOOKUP_OUTPUT)))
+ return false;
+
+ return true;
+}
BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,
struct bpf_fib_lookup *, params, int, plen, u32, flags)
@@ -6522,7 +6578,7 @@ BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,
if (plen < sizeof(*params))
return -EINVAL;
- if (flags & ~BPF_FIB_LOOKUP_MASK)
+ if (!bpf_fib_lookup_flags_ok(flags))
return -EINVAL;
switch (params->family) {
@@ -6560,7 +6616,7 @@ BPF_CALL_4(bpf_skb_fib_lookup, struct sk_buff *, skb,
if (plen < sizeof(*params))
return -EINVAL;
- if (flags & ~BPF_FIB_LOOKUP_MASK)
+ if (!bpf_fib_lookup_flags_ok(flags))
return -EINVAL;
if (flags & BPF_FIB_LOOKUP_VLAN)
diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h
index e00f0392e728..d4218954c50f 100644
--- a/tools/include/uapi/linux/bpf.h
+++ b/tools/include/uapi/linux/bpf.h
@@ -3555,6 +3555,22 @@ union bpf_attr {
* This flag is only valid for XDP programs; tc programs
* receive -EINVAL since they can redirect to the VLAN
* device directly.
+ * **BPF_FIB_LOOKUP_VLAN_INPUT**
+ * Treat *params*->h_vlan_proto and *params*->h_vlan_TCI
+ * as an input VLAN tag and run the lookup as if ingress
+ * had happened on the VLAN subinterface carrying that tag
+ * on *params*->ifindex. The VID is the low 12 bits of
+ * *params*->h_vlan_TCI; *params*->h_vlan_proto must be
+ * ETH_P_8021Q or ETH_P_8021AD in network byte order, else
+ * **-EINVAL**. If *params*->ifindex is itself a VLAN
+ * device, its inner (QinQ) subinterface is matched; for a
+ * bond or team, pass the master's ifindex. An unmatched
+ * tag, a down device, or one in another namespace returns
+ * **BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress.
+ * A VID of 0 is looked up literally, so do not set this
+ * flag for priority-tagged frames. Cannot be combined with
+ * **BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT**
+ * (returns **-EINVAL**).
*
* *ctx* is either **struct xdp_md** for XDP programs or
* **struct sk_buff** tc cls_act programs.
@@ -7351,6 +7367,7 @@ enum {
BPF_FIB_LOOKUP_SRC = (1U << 4),
BPF_FIB_LOOKUP_MARK = (1U << 5),
BPF_FIB_LOOKUP_VLAN = (1U << 6),
+ BPF_FIB_LOOKUP_VLAN_INPUT = (1U << 7),
};
enum {
@@ -7421,7 +7438,9 @@ struct bpf_fib_lookup {
/*
* output with BPF_FIB_LOOKUP_VLAN: set from the
* resolved egress VLAN device (see the flag); zeroed
- * on other successful lookups.
+ * on other successful lookups. input with
+ * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope
+ * the lookup by.
*/
__be16 h_vlan_proto;
__be16 h_vlan_TCI;
--
2.54.0
^ permalink raw reply related
* [PATCH bpf-next v6 3/3] selftests/bpf: Add bpf_fib_lookup() VLAN flag tests
From: Avinash Duduskar @ 2026-07-04 9:21 UTC (permalink / raw)
To: ast, daniel, andrii
Cc: eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil,
john.fastabend, sdf, davem, edumazet, kuba, pabeni, horms, shuah,
hawk, yatsenko, leon.hwang, kpsingh, a.s.protopopov, ameryhung,
rongtao, eyal.birger, bpf, netdev, linux-kernel, linux-kselftest,
toke, dsahern
In-Reply-To: <20260704092159.1256823-1-avinash.duduskar@gmail.com>
Cover both new VLAN flags in the fib_lookup test. BPF_FIB_LOOKUP_VLAN
reduces a VLAN egress to its physical parent plus the tag, and
BPF_FIB_LOOKUP_VLAN_INPUT scopes the lookup to a VLAN subinterface.
BPF_FIB_LOOKUP_VLAN is XDP-only, since VLAN devices have no XDP xmit; the
tc helper rejects it with -EINVAL, which the table runner asserts for
every flag arm, and the egress result is checked through
bpf_xdp_fib_lookup(). Non-VLAN cases run through both helpers and assert
the path-independent results match; the XDP loop also checks dmac and,
for the tot_len cases, the route mtu_result, so the VLAN-egress dmac and
frag-needed coverage stays even though the tc path no longer reaches it.
The egress arms pin the reduction (parent ifindex plus tag, including
via a neighbour on the VLAN device, in OUTPUT mode, over a bond, and
through a DIRECT|TBID table) and the failure contract: a stacked-VLAN
(QinQ) egress returns BPF_FIB_LKUP_RET_VLAN_FAILURE with params->ifindex
left at the input. That is distinct from a no-neighbour return, which
reports the egress ifindex; only VLAN_FAILURE rewinds params->ifindex,
and a guard arm whose input and egress devices differ pins the
distinction. The VLAN_FAILURE arms are IPv4; the IPv6 path reaches it
through the same shared code, so an IPv6 arm would only re-test that.
The input arms use an iif rule that routes one destination to two
gateways, so the asserted gateway reveals which device the lookup used
as ingress, including VRF table selection through the l3mdev rule and
l3mdev_fib_table_rcu(). A cross-netns subtest moves a VLAN device into a
second netns while it stays registered on its parent and checks both
directions fail closed at the boundary.
A live-frames subtest (test_fib_lookup_vlan_redirect, with
BPF_F_TEST_XDP_LIVE_FRAMES) drives real frames through the native
xdp_do_redirect() / xdp_do_flush() path: a reducible egress is
redirected to the parent and delivered to its peer, while a QinQ egress
is passed to the stack, since redirecting to the VLAN device would drop
the frame at flush (no ndo_xdp_xmit).
The remaining per-case assertions are in the test table: resolution
semantics, the -EINVAL and NOT_FWDED error arms, and the SRC/SKIP_NEIGH
combinations.
Signed-off-by: Avinash Duduskar <avinash.duduskar@gmail.com>
---
.../selftests/bpf/prog_tests/fib_lookup.c | 712 +++++++++++++++++-
.../testing/selftests/bpf/progs/fib_lookup.c | 36 +
2 files changed, 744 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/fib_lookup.c b/tools/testing/selftests/bpf/prog_tests/fib_lookup.c
index bd7658958004..18b6fdeca382 100644
--- a/tools/testing/selftests/bpf/prog_tests/fib_lookup.c
+++ b/tools/testing/selftests/bpf/prog_tests/fib_lookup.c
@@ -2,6 +2,7 @@
/* Copyright (c) 2023 Meta Platforms, Inc. and affiliates. */
#include <linux/rtnetlink.h>
+#include <linux/if_ether.h>
#include <sys/types.h>
#include <net/if.h>
@@ -23,6 +24,7 @@
#define IPV4_TBID_ADDR "172.0.0.254"
#define IPV4_TBID_NET "172.0.0.0"
#define IPV4_TBID_DST "172.0.0.2"
+#define IPV4_TBID_NONEIGH_DST "172.0.0.5"
#define IPV6_TBID_ADDR "fd00::FFFF"
#define IPV6_TBID_NET "fd00::"
#define IPV6_TBID_DST "fd00::2"
@@ -37,6 +39,41 @@
#define IPV6_LOCAL "fd01::3"
#define IPV6_GW1 "fd01::1"
#define IPV6_GW2 "fd01::2"
+#define VLAN_ID 100
+#define VLAN_IFACE "veth1.100"
+#define VLAN_ID_DOWN 102
+#define VLAN_IFACE_DOWN "veth1.102"
+#define QINQ_OUTER_IFACE "veth1.200"
+#define QINQ_INNER_IFACE "veth1.200.300"
+#define VLAN_TABLE "300"
+#define IPV4_VLAN_IFACE_ADDR "10.5.0.254"
+#define IPV4_VLAN_EGRESS_DST "10.5.0.2"
+#define IPV4_QINQ_DST "10.7.0.2"
+#define IPV4_VLAN_DST "10.6.0.2"
+#define IPV4_VLAN_GW "10.5.0.1"
+#define IPV6_VLAN_IFACE_ADDR "fd02::254"
+#define IPV6_VLAN_EGRESS_DST "fd02::2"
+#define IPV6_VLAN_DST "fd03::2"
+#define IPV6_VLAN_GW "fd02::1"
+#define VLAN_VID_UNUSED 999
+#define VRF_IFACE "vrf-blue"
+#define VRF_TABLE "1000"
+#define VRF_VLAN_ID 101
+#define VRF_VLAN_IFACE "veth1.101"
+#define IPV4_VRF_IFACE_ADDR "10.8.0.254"
+#define IPV4_VRF_GW "10.8.0.1"
+#define IPV4_VRF_DST "10.9.0.2"
+#define TBID_VLAN_ID 50
+#define TBID_VLAN_IFACE "veth2.50"
+#define IPV4_TBID_VLAN_DST "172.2.0.2"
+#define IPV4_BOND_VLAN_DST "10.11.0.2"
+#define IPV4_VLAN_MTU_DST "10.5.9.2"
+#define QINQ_AD_VLAN_ID 200
+#define QINQ_INNER_VLAN_ID 300
+#define BOND_IFACE "bond99"
+#define BOND_PORT "veth3"
+#define BOND_PORT_PEER "veth4"
+#define BOND_VLAN_ID 500
#define DMAC "11:11:11:11:11:11"
#define DMAC_INIT { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, }
#define DMAC2 "01:01:01:01:01:01"
@@ -52,6 +89,17 @@ struct fib_lookup_test {
__u32 tbid;
__u8 dmac[6];
__u32 mark;
+ /*
+ * input tag with BPF_FIB_LOOKUP_VLAN_INPUT; expected output tag
+ * with BPF_FIB_LOOKUP_VLAN (checked when check_vlan is set)
+ */
+ __u16 vlan_proto;
+ __u16 vlan_id;
+ bool check_vlan;
+ const char *expected_dev; /* expected params->ifindex after lookup */
+ const char *iif; /* override the default veth1 input device */
+ __u16 tot_len; /* triggers the in-lookup mtu check when set */
+ __u16 expected_mtu; /* expected mtu_result (union with tot_len) */
};
static const struct fib_lookup_test tests[] = {
@@ -79,6 +127,17 @@ static const struct fib_lookup_test tests[] = {
.daddr = IPV4_TBID_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
.lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID, .tbid = 100,
.dmac = DMAC_INIT2, },
+ /*
+ * An error that returns after the egress device is resolved must
+ * report the egress ifindex, not the input. This routes from input
+ * veth1 via veth2 (table 100) to a dst with no neighbour, so
+ * input != egress, pinning NO_NEIGH to the egress device.
+ */
+ { .desc = "IPv4 NO_NEIGH reports the egress ifindex, not the input",
+ .daddr = IPV4_TBID_NONEIGH_DST,
+ .expected_ret = BPF_FIB_LKUP_RET_NO_NEIGH,
+ .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID, .tbid = 100,
+ .expected_dev = "veth2", },
{ .desc = "IPv6 TBID lookup failure",
.daddr = IPV6_TBID_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
.lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID,
@@ -142,6 +201,218 @@ static const struct fib_lookup_test tests[] = {
.expected_dst = IPV6_GW1,
.lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
.mark = MARK, },
+ /* vlan egress resolution */
+ /*
+ * Invariant the VLAN-egress arms jointly enforce: a
+ * BPF_FIB_LOOKUP_VLAN SUCCESS always carries a physical,
+ * xmit-capable ifindex; no SUCCESS ever returns a VLAN-device
+ * ifindex. Reducible arms pin ifindex == the physical parent; the
+ * QinQ and foreign-netns arms pin VLAN_FAILURE with params->ifindex
+ * left at the input, so a regression to best-effort (SUCCESS + the
+ * VLAN ifindex) fails one.
+ */
+ { .desc = "IPv4 VLAN egress, no flag",
+ .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = VLAN_IFACE, .check_vlan = true, },
+ { .desc = "IPv4 VLAN egress, single VLAN",
+ .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ /*
+ * skb path without tot_len: mtu_result is the VLAN device's mtu
+ * (1400), not the parent's (1500)
+ */
+ { .desc = "IPv4 VLAN egress, skb-path mtu is the VLAN device's without the flag",
+ .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = VLAN_IFACE, .check_vlan = true, .expected_mtu = 1400, },
+ { .desc = "IPv4 VLAN egress, flag set but egress is not a VLAN",
+ .daddr = IPV4_NUD_FAILED_ADDR, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true, },
+ { .desc = "IPv4 VLAN egress, QinQ not reducible (VLAN_FAILURE)",
+ .daddr = IPV4_QINQ_DST,
+ .expected_ret = BPF_FIB_LKUP_RET_VLAN_FAILURE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true, },
+ { .desc = "IPv4 QinQ egress without the flag (escape hatch)",
+ .daddr = IPV4_QINQ_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = QINQ_INNER_IFACE, },
+ { .desc = "IPv6 VLAN egress, single VLAN",
+ .daddr = IPV6_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN egress, neighbour on the VLAN device",
+ .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN,
+ .expected_dev = "veth1", .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, .dmac = DMAC_INIT, },
+ { .desc = "IPv4 VLAN egress in OUTPUT mode",
+ .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .iif = VLAN_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_OUTPUT | BPF_FIB_LOOKUP_VLAN |
+ BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN egress over a bond",
+ .daddr = IPV4_BOND_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = BOND_IFACE, .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },
+ { .desc = "IPv4 VLAN egress via TBID table",
+ .daddr = IPV4_TBID_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID |
+ BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .tbid = 100,
+ .expected_dev = "veth2", .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = TBID_VLAN_ID, },
+ { .desc = "IPv4 VLAN egress, success writes mtu_result with the swap",
+ .daddr = IPV4_VLAN_MTU_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .tot_len = 500, .expected_mtu = 1000,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN egress, FRAG_NEEDED reports mtu, swap unwritten",
+ .daddr = IPV4_VLAN_MTU_DST, .expected_ret = BPF_FIB_LKUP_RET_FRAG_NEEDED,
+ .tot_len = 1400, .expected_mtu = 1000,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .expected_dev = "veth1", .check_vlan = true, },
+ /* vlan tag as lookup input */
+ { .desc = "IPv4 VLAN input, no flag",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_GW1,
+ .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH, },
+ { .desc = "IPv4 VLAN input, tag selects subinterface route",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_VLAN_GW, .expected_dev = VLAN_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv6 VLAN input, tag selects subinterface route",
+ .daddr = IPV6_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV6_VLAN_GW, .expected_dev = VLAN_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN input and egress combined",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_VLAN_GW, .expected_dev = "veth1",
+ .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_VLAN |
+ BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN input, neighbour resolved on the route",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_VLAN_GW, .expected_dev = VLAN_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, .dmac = DMAC_INIT2, },
+ { .desc = "IPv4 VLAN input, source address from the subinterface",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_src = IPV4_VLAN_IFACE_ADDR,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SRC |
+ BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ /*
+ * VRF: the resolved subinterface is enslaved, so the l3mdev rule
+ * (full lookup) and l3mdev_fib_table_rcu() (DIRECT) must select
+ * the VRF table from the resolved ingress
+ */
+ { .desc = "IPv4 VLAN input, VRF subinterface, no flag",
+ .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_GW1,
+ .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH, },
+ { .desc = "IPv4 VLAN input, tag selects VRF table",
+ .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_VRF_GW, .expected_dev = VRF_VLAN_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VRF_VLAN_ID, },
+ { .desc = "IPv4 VLAN input, DIRECT uses VRF table from resolved ingress",
+ .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_VRF_GW, .expected_dev = VRF_VLAN_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_DIRECT |
+ BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VRF_VLAN_ID, },
+ /*
+ * failure arms also assert params is left untouched: ifindex still
+ * names the physical device and the input tag bytes survive
+ */
+ { .desc = "IPv4 VLAN input, invalid proto",
+ .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+ .expected_dev = "veth1", .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = 0x1234, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN input, unmatched VID",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+ .expected_dev = "veth1", .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_VID_UNUSED, },
+ { .desc = "IPv4 VLAN input, subinterface down",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+ .expected_dev = "veth1", .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID_DOWN, },
+ /*
+ * the resolver runs before the forwarding check, so on devices
+ * with forwarding off FWD_DISABLED (not NOT_FWDED) proves the tag
+ * resolved to that device and the lookup used it as ingress
+ */
+ { .desc = "IPv4 VLAN input, 802.1ad tag",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021AD, .vlan_id = QINQ_AD_VLAN_ID, },
+ { .desc = "IPv4 VLAN input, PCP and DEI bits ignored in TCI",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+ .expected_dst = IPV4_VLAN_GW,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = 0xe000 | VLAN_ID, },
+ { .desc = "IPv4 VLAN input, inner QinQ device from VLAN ifindex",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,
+ .iif = QINQ_OUTER_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = QINQ_INNER_VLAN_ID, },
+ /*
+ * bonding: the VLANs live on the master, as on receive, where the
+ * frame is steered to the master before VLAN processing; a port
+ * ifindex does not match (ports carry vid state but no VLAN devs)
+ */
+ { .desc = "IPv4 VLAN input, tag on bond master resolves",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,
+ .iif = BOND_IFACE,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },
+ { .desc = "IPv4 VLAN input, tag on bond port does not match",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+ .iif = BOND_PORT, .expected_dev = BOND_PORT, .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },
+ { .desc = "IPv6 VLAN input, invalid proto",
+ .daddr = IPV6_VLAN_DST, .expected_ret = -EINVAL,
+ .expected_dev = "veth1", .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = 0x1234, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN input, VID 0 priority tag fails closed",
+ .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+ .expected_dev = "veth1", .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = 0, },
+ { .desc = "IPv6 VLAN input, unmatched VID",
+ .daddr = IPV6_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+ .expected_dev = "veth1", .check_vlan = true,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_VID_UNUSED, },
+ { .desc = "unknown flag bit rejected",
+ .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+ .lookup_flags = (1 << 14) | BPF_FIB_LOOKUP_SKIP_NEIGH, },
+ { .desc = "IPv4 VLAN input rejected with TBID",
+ .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_TBID,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+ { .desc = "IPv4 VLAN input rejected with OUTPUT",
+ .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+ .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_OUTPUT,
+ .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
};
static int setup_netns(void)
@@ -204,6 +475,105 @@ static int setup_netns(void)
SYS(fail, "ip rule add prio 2 fwmark %d lookup %s", MARK, MARK_TABLE);
SYS(fail, "ip -6 rule add prio 2 fwmark %d lookup %s", MARK, MARK_TABLE);
+ /*
+ * Setup for vlan tests: a subinterface for egress resolution and
+ * tag-as-input, a QinQ stack, and an iif rule so the input tests
+ * observe which device the lookup used as ingress.
+ */
+ SYS(fail, "ip link add link veth1 name %s type vlan id %d",
+ VLAN_IFACE, VLAN_ID);
+ SYS(fail, "ip link set dev %s up", VLAN_IFACE);
+ /*
+ * lower than the veth1 parent (1500): the skb-path mtu check uses the
+ * FIB result (VLAN) device, so mtu_result is this value with or
+ * without the egress swap, which two arms below pin
+ */
+ SYS(fail, "ip link set dev %s mtu 1400", VLAN_IFACE);
+ SYS(fail, "ip addr add %s/24 dev %s", IPV4_VLAN_IFACE_ADDR, VLAN_IFACE);
+ SYS(fail, "ip addr add %s/64 dev %s nodad", IPV6_VLAN_IFACE_ADDR, VLAN_IFACE);
+
+ /*
+ * stays down: the input flag must treat its tag the way real
+ * ingress treats a frame arriving on a down VLAN device (drop)
+ */
+ SYS(fail, "ip link add link veth1 name %s type vlan id %d",
+ VLAN_IFACE_DOWN, VLAN_ID_DOWN);
+
+ err = write_sysctl("/proc/sys/net/ipv4/conf/" VLAN_IFACE "/forwarding", "1");
+ if (!ASSERT_OK(err, "write_sysctl(net.ipv4.conf." VLAN_IFACE ".forwarding)"))
+ goto fail;
+
+ err = write_sysctl("/proc/sys/net/ipv6/conf/" VLAN_IFACE "/forwarding", "1");
+ if (!ASSERT_OK(err, "write_sysctl(net.ipv6.conf." VLAN_IFACE ".forwarding)"))
+ goto fail;
+
+ SYS(fail, "ip link add link veth1 name %s type vlan proto 802.1ad id 200",
+ QINQ_OUTER_IFACE);
+ SYS(fail, "ip link add link %s name %s type vlan id 300",
+ QINQ_OUTER_IFACE, QINQ_INNER_IFACE);
+ SYS(fail, "ip link set dev %s up", QINQ_OUTER_IFACE);
+ SYS(fail, "ip link set dev %s up", QINQ_INNER_IFACE);
+ SYS(fail, "ip route add %s/32 dev %s", IPV4_QINQ_DST, QINQ_INNER_IFACE);
+
+ SYS(fail, "ip route add %s/32 via %s", IPV4_VLAN_DST, IPV4_GW1);
+ SYS(fail, "ip route add table %s %s/32 via %s",
+ VLAN_TABLE, IPV4_VLAN_DST, IPV4_VLAN_GW);
+ SYS(fail, "ip rule add prio 3 iif %s lookup %s", VLAN_IFACE, VLAN_TABLE);
+ SYS(fail, "ip -6 route add %s/128 via %s", IPV6_VLAN_DST, IPV6_GW1);
+ SYS(fail, "ip -6 route add table %s %s/128 via %s",
+ VLAN_TABLE, IPV6_VLAN_DST, IPV6_VLAN_GW);
+ SYS(fail, "ip -6 rule add prio 3 iif %s lookup %s", VLAN_IFACE, VLAN_TABLE);
+
+ /* a bond with one port and a VLAN on the bond */
+ SYS(fail, "ip link add %s type bond", BOND_IFACE);
+ SYS(fail, "ip link add %s type veth peer name %s", BOND_PORT, BOND_PORT_PEER);
+ SYS(fail, "ip link set %s master %s", BOND_PORT, BOND_IFACE);
+ SYS(fail, "ip link set dev %s up", BOND_IFACE);
+ SYS(fail, "ip link set dev %s up", BOND_PORT);
+ SYS(fail, "ip link add link %s name %s.%d type vlan id %d",
+ BOND_IFACE, BOND_IFACE, BOND_VLAN_ID, BOND_VLAN_ID);
+ SYS(fail, "ip link set dev %s.%d up", BOND_IFACE, BOND_VLAN_ID);
+ SYS(fail, "ip route add %s/32 dev %s.%d",
+ IPV4_BOND_VLAN_DST, BOND_IFACE, BOND_VLAN_ID);
+
+ /*
+ * a VRF with its own dedicated subinterface (the iif rules above
+ * must not see it), for the table-selection-by-ingress cases
+ */
+ SYS(fail, "ip link add %s type vrf table %s", VRF_IFACE, VRF_TABLE);
+ SYS(fail, "ip link set dev %s up", VRF_IFACE);
+ SYS(fail, "ip link add link veth1 name %s type vlan id %d",
+ VRF_VLAN_IFACE, VRF_VLAN_ID);
+ SYS(fail, "ip link set %s master %s", VRF_VLAN_IFACE, VRF_IFACE);
+ SYS(fail, "ip link set dev %s up", VRF_VLAN_IFACE);
+ SYS(fail, "ip addr add %s/24 dev %s", IPV4_VRF_IFACE_ADDR, VRF_VLAN_IFACE);
+ err = write_sysctl("/proc/sys/net/ipv4/conf/" VRF_VLAN_IFACE "/forwarding", "1");
+ if (!ASSERT_OK(err, "write_sysctl(net.ipv4.conf." VRF_VLAN_IFACE ".forwarding)"))
+ goto fail;
+ SYS(fail, "ip route add %s/32 via %s", IPV4_VRF_DST, IPV4_GW1);
+ SYS(fail, "ip route add table %s %s/32 via %s",
+ VRF_TABLE, IPV4_VRF_DST, IPV4_VRF_GW);
+
+ /* neighbours on the VLAN subinterface for the non-SKIP_NEIGH cases */
+ err = write_sysctl("/proc/sys/net/ipv4/neigh/" VLAN_IFACE "/gc_stale_time", "900");
+ if (!ASSERT_OK(err, "write_sysctl(net.ipv4.neigh." VLAN_IFACE ".gc_stale_time)"))
+ goto fail;
+ SYS(fail, "ip neigh add %s dev %s lladdr %s nud stale",
+ IPV4_VLAN_EGRESS_DST, VLAN_IFACE, DMAC);
+ SYS(fail, "ip neigh add %s dev %s lladdr %s nud stale",
+ IPV4_VLAN_GW, VLAN_IFACE, DMAC2);
+
+ /* a VLAN on veth2 with a route in the tbid test table */
+ SYS(fail, "ip link add link veth2 name %s type vlan id %d",
+ TBID_VLAN_IFACE, TBID_VLAN_ID);
+ SYS(fail, "ip link set dev %s up", TBID_VLAN_IFACE);
+ SYS(fail, "ip route add table 100 %s/32 dev %s",
+ IPV4_TBID_VLAN_DST, TBID_VLAN_IFACE);
+
+ /* a locked-mtu route via the subinterface for the FRAG_NEEDED case */
+ SYS(fail, "ip route add %s/32 dev %s mtu lock 1000",
+ IPV4_VLAN_MTU_DST, VLAN_IFACE);
+
return 0;
fail:
return -1;
@@ -218,9 +588,16 @@ static int set_lookup_params(struct bpf_fib_lookup *params,
memset(params, 0, sizeof(*params));
params->l4_protocol = IPPROTO_TCP;
- params->ifindex = ifindex;
+ params->ifindex = test->iif ? if_nametoindex(test->iif) : ifindex;
params->tbid = test->tbid;
params->mark = test->mark;
+ params->tot_len = test->tot_len;
+
+ /* h_vlan_proto/h_vlan_TCI union with tbid */
+ if (test->lookup_flags & BPF_FIB_LOOKUP_VLAN_INPUT) {
+ params->h_vlan_proto = htons(test->vlan_proto);
+ params->h_vlan_TCI = htons(test->vlan_id);
+ }
if (inet_pton(AF_INET6, test->daddr, params->ipv6_dst) == 1) {
params->family = AF_INET6;
@@ -298,7 +675,7 @@ void test_fib_lookup(void)
struct nstoken *nstoken = NULL;
struct __sk_buff skb = { };
struct fib_lookup *skel;
- int prog_fd, err, ret, i;
+ int prog_fd, xdp_fd, err, ret, i;
/* The test does not use the skb->data, so
* use pkt_v6 for both v6 and v4 test.
@@ -309,11 +686,16 @@ void test_fib_lookup(void)
.ctx_in = &skb,
.ctx_size_in = sizeof(skb),
);
+ LIBBPF_OPTS(bpf_test_run_opts, xdp_opts,
+ .data_in = &pkt_v6,
+ .data_size_in = sizeof(pkt_v6),
+ );
skel = fib_lookup__open_and_load();
if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
return;
prog_fd = bpf_program__fd(skel->progs.fib_lookup);
+ xdp_fd = bpf_program__fd(skel->progs.fib_lookup_xdp);
SYS(fail, "ip netns add %s", NS_TEST);
@@ -343,6 +725,16 @@ void test_fib_lookup(void)
if (!ASSERT_OK(err, "bpf_prog_test_run_opts"))
continue;
+ /*
+ * BPF_FIB_LOOKUP_VLAN is XDP-only; the tc helper rejects it.
+ * These cases are exercised on the XDP path below.
+ */
+ if (tests[i].lookup_flags & BPF_FIB_LOOKUP_VLAN) {
+ ASSERT_EQ(skel->bss->fib_lookup_ret, -EINVAL,
+ "tc rejects BPF_FIB_LOOKUP_VLAN");
+ continue;
+ }
+
ASSERT_EQ(skel->bss->fib_lookup_ret, tests[i].expected_ret,
"fib_lookup_ret");
@@ -352,6 +744,21 @@ void test_fib_lookup(void)
if (tests[i].expected_dst)
assert_dst_ip(fib_params, tests[i].expected_dst);
+ if (tests[i].expected_dev)
+ ASSERT_EQ(fib_params->ifindex,
+ if_nametoindex(tests[i].expected_dev), "ifindex");
+
+ if (tests[i].expected_mtu)
+ ASSERT_EQ(fib_params->mtu_result, tests[i].expected_mtu,
+ "mtu_result");
+
+ if (tests[i].check_vlan) {
+ ASSERT_EQ(fib_params->h_vlan_proto,
+ htons(tests[i].vlan_proto), "h_vlan_proto");
+ ASSERT_EQ(fib_params->h_vlan_TCI,
+ htons(tests[i].vlan_id), "h_vlan_TCI");
+ }
+
ret = memcmp(tests[i].dmac, fib_params->dmac, sizeof(tests[i].dmac));
if (!ASSERT_EQ(ret, 0, "dmac not match")) {
char expected[18], actual[18];
@@ -361,15 +768,312 @@ void test_fib_lookup(void)
printf("dmac expected %s actual %s ", expected, actual);
}
- // ensure tbid is zero'd out after fib lookup.
- if (tests[i].lookup_flags & BPF_FIB_LOOKUP_DIRECT) {
+ /*
+ * ensure tbid is zero'd out after fib lookup. With
+ * BPF_FIB_LOOKUP_VLAN the union holds the packed vlan
+ * fields instead, so skip the check for those.
+ */
+ if ((tests[i].lookup_flags & BPF_FIB_LOOKUP_DIRECT) &&
+ !(tests[i].lookup_flags & BPF_FIB_LOOKUP_VLAN)) {
if (!ASSERT_EQ(skel->bss->fib_params.tbid, 0,
"expected fib_params.tbid to be zero"))
goto fail;
}
}
+ /*
+ * Re-run the cases through bpf_xdp_fib_lookup(). test_run uses the
+ * current netns' loopback for ctx->rxq->dev, so dev_net() is NS_TEST
+ * and the lookup runs against its FIB. The path-independent results
+ * (return code, swapped ifindex, vlan tag, gateway) must match the skb
+ * path; the no-tot_len mtu_result is skb-specific and not rechecked.
+ */
+ for (i = 0; i < ARRAY_SIZE(tests); i++) {
+ if (set_lookup_params(fib_params, &tests[i], skb.ifindex))
+ continue;
+
+ skel->bss->fib_lookup_ret = -1;
+ skel->bss->lookup_flags = tests[i].lookup_flags;
+
+ err = bpf_prog_test_run_opts(xdp_fd, &xdp_opts);
+ if (!ASSERT_OK(err, "xdp test_run"))
+ continue;
+
+ if (!ASSERT_EQ(skel->bss->fib_lookup_ret, tests[i].expected_ret,
+ "xdp fib_lookup_ret"))
+ printf("(xdp) %s\n", tests[i].desc);
+
+ if (tests[i].expected_dev)
+ ASSERT_EQ(fib_params->ifindex,
+ if_nametoindex(tests[i].expected_dev),
+ "xdp ifindex");
+
+ if (tests[i].expected_dst)
+ assert_dst_ip(fib_params, tests[i].expected_dst);
+
+ if (tests[i].check_vlan) {
+ ASSERT_EQ(fib_params->h_vlan_proto,
+ htons(tests[i].vlan_proto), "xdp h_vlan_proto");
+ ASSERT_EQ(fib_params->h_vlan_TCI,
+ htons(tests[i].vlan_id), "xdp h_vlan_TCI");
+ }
+
+ ret = memcmp(tests[i].dmac, fib_params->dmac, sizeof(tests[i].dmac));
+ ASSERT_EQ(ret, 0, "xdp dmac");
+
+ /*
+ * mtu_result from a tot_len lookup is the route mtu and is
+ * path-independent; the no-tot_len arm reads dev->mtu and is
+ * skb-only, so gate on tot_len
+ */
+ if (tests[i].expected_mtu && tests[i].tot_len)
+ ASSERT_EQ(fib_params->mtu_result, tests[i].expected_mtu,
+ "xdp mtu_result");
+ }
+
+fail:
+ if (nstoken)
+ close_netns(nstoken);
+ SYS_NOFAIL("ip netns del " NS_TEST);
+ fib_lookup__destroy(skel);
+}
+
+#define NS_VLAN_A "fib_lookup_vlan_ns_a"
+#define NS_VLAN_B "fib_lookup_vlan_ns_b"
+
+/*
+ * A VLAN device can be moved to another netns while staying registered
+ * on its parent. Neither direction may then cross the boundary: the
+ * egress flag must not publish the foreign parent's ifindex, and the
+ * input flag must fail closed rather than use a foreign ingress.
+ */
+void test_fib_lookup_vlan_netns(void)
+{
+ struct bpf_fib_lookup *fib_params;
+ struct nstoken *nstoken = NULL;
+ struct __sk_buff skb = { };
+ struct fib_lookup *skel = NULL;
+ int prog_fd, xdp_fd, err, parent_idx, vlan_idx;
+
+ LIBBPF_OPTS(bpf_test_run_opts, run_opts,
+ .data_in = &pkt_v6,
+ .data_size_in = sizeof(pkt_v6),
+ .ctx_in = &skb,
+ .ctx_size_in = sizeof(skb),
+ );
+ LIBBPF_OPTS(bpf_test_run_opts, xdp_opts,
+ .data_in = &pkt_v6,
+ .data_size_in = sizeof(pkt_v6),
+ );
+
+ skel = fib_lookup__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+ return;
+ prog_fd = bpf_program__fd(skel->progs.fib_lookup);
+ xdp_fd = bpf_program__fd(skel->progs.fib_lookup_xdp);
+ fib_params = &skel->bss->fib_params;
+
+ SYS(fail, "ip netns add %s", NS_VLAN_A);
+ SYS(fail, "ip netns add %s", NS_VLAN_B);
+
+ nstoken = open_netns(NS_VLAN_A);
+ if (!ASSERT_OK_PTR(nstoken, "open_netns(a)"))
+ goto fail;
+
+ SYS(fail, "ip link add veth7 type veth peer name veth8");
+ SYS(fail, "ip link set dev veth7 up");
+ SYS(fail, "ip link add link veth7 name veth7.66 type vlan id 66");
+ SYS(fail, "ip link set veth7.66 netns %s", NS_VLAN_B);
+
+ parent_idx = if_nametoindex("veth7");
+ if (!ASSERT_NEQ(parent_idx, 0, "if_nametoindex(veth7)"))
+ goto fail;
+
+ /*
+ * input: the moved device is still in veth7's VLAN group, but it
+ * lives in another netns, so the lookup must fail closed
+ */
+ skb.ifindex = parent_idx;
+ memset(fib_params, 0, sizeof(*fib_params));
+ fib_params->family = AF_INET;
+ fib_params->l4_protocol = IPPROTO_TCP;
+ fib_params->ifindex = parent_idx;
+ fib_params->h_vlan_proto = htons(ETH_P_8021Q);
+ fib_params->h_vlan_TCI = htons(66);
+ if (!ASSERT_EQ(inet_pton(AF_INET, "10.66.0.2", &fib_params->ipv4_dst),
+ 1, "inet_pton(dst)"))
+ goto fail;
+
+ skel->bss->fib_lookup_ret = -1;
+ skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT |
+ BPF_FIB_LOOKUP_SKIP_NEIGH;
+ err = bpf_prog_test_run_opts(prog_fd, &run_opts);
+ if (!ASSERT_OK(err, "test_run(input)"))
+ goto fail;
+ ASSERT_EQ(skel->bss->fib_lookup_ret, BPF_FIB_LKUP_RET_NOT_FWDED,
+ "input across netns fails closed");
+ ASSERT_EQ(fib_params->ifindex, parent_idx, "ifindex untouched");
+ ASSERT_EQ(fib_params->h_vlan_TCI, htons(66), "tag untouched");
+
+ close_netns(nstoken);
+ nstoken = open_netns(NS_VLAN_B);
+ if (!ASSERT_OK_PTR(nstoken, "open_netns(b)"))
+ goto fail;
+
+ /*
+ * egress: the fib result is the VLAN device here, but its parent
+ * is in the other netns, so the swap must not happen
+ */
+ SYS(fail, "ip link set dev veth7.66 up");
+ SYS(fail, "ip addr add 10.66.0.1/24 dev veth7.66");
+ err = write_sysctl("/proc/sys/net/ipv4/conf/veth7.66/forwarding", "1");
+ if (!ASSERT_OK(err, "write_sysctl(forwarding)"))
+ goto fail;
+
+ vlan_idx = if_nametoindex("veth7.66");
+ if (!ASSERT_NEQ(vlan_idx, 0, "if_nametoindex(veth7.66)"))
+ goto fail;
+
+ memset(fib_params, 0, sizeof(*fib_params));
+ fib_params->family = AF_INET;
+ fib_params->l4_protocol = IPPROTO_TCP;
+ fib_params->ifindex = vlan_idx;
+ if (!ASSERT_EQ(inet_pton(AF_INET, "10.66.0.2", &fib_params->ipv4_dst),
+ 1, "inet_pton(dst)") ||
+ !ASSERT_EQ(inet_pton(AF_INET, "10.66.0.1", &fib_params->ipv4_src),
+ 1, "inet_pton(src)"))
+ goto fail;
+
+ skel->bss->fib_lookup_ret = -1;
+ skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN |
+ BPF_FIB_LOOKUP_SKIP_NEIGH;
+ err = bpf_prog_test_run_opts(xdp_fd, &xdp_opts);
+ if (!ASSERT_OK(err, "test_run(egress)"))
+ goto fail;
+ ASSERT_EQ(skel->bss->fib_lookup_ret, BPF_FIB_LKUP_RET_VLAN_FAILURE,
+ "egress returns VLAN_FAILURE");
+ ASSERT_EQ(fib_params->ifindex, vlan_idx,
+ "foreign parent not published");
+ ASSERT_EQ(fib_params->h_vlan_TCI, 0, "vlan fields zero");
+
+fail:
+ if (nstoken)
+ close_netns(nstoken);
+ SYS_NOFAIL("ip netns del " NS_VLAN_A);
+ SYS_NOFAIL("ip netns del " NS_VLAN_B);
+ fib_lookup__destroy(skel);
+}
+
+#define REDIRECT_NPKTS 1000
+
+/*
+ * The egress flag exists so an XDP program can redirect to the physical
+ * parent. A redirect that lands on a VLAN device is dropped at
+ * xdp_do_flush(), because a VLAN device has no ndo_xdp_xmit. Drive real
+ * frames with BPF_F_TEST_XDP_LIVE_FRAMES, which runs the native
+ * xdp_do_redirect() + xdp_do_flush() path: a reducible VLAN egress
+ * resolves to veth1 and is delivered to its peer veth2, while a QinQ
+ * egress returns VLAN_FAILURE and is passed to the stack instead of
+ * redirected to a device that would silently drop it.
+ */
+void test_fib_lookup_vlan_redirect(void)
+{
+ int redirect_fd, err, veth1_idx, veth2_idx = -1;
+ struct bpf_fib_lookup *fib_params;
+ struct nstoken *nstoken = NULL;
+ struct fib_lookup *skel = NULL;
+ bool xdp_attached = false;
+
+ LIBBPF_OPTS(bpf_test_run_opts, lf_opts,
+ .data_in = &pkt_v4,
+ .data_size_in = sizeof(pkt_v4),
+ .flags = BPF_F_TEST_XDP_LIVE_FRAMES,
+ .repeat = REDIRECT_NPKTS,
+ );
+
+ skel = fib_lookup__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+ return;
+ redirect_fd = bpf_program__fd(skel->progs.fib_lookup_redirect);
+ fib_params = &skel->bss->fib_params;
+
+ SYS(fail, "ip netns add %s", NS_TEST);
+ nstoken = open_netns(NS_TEST);
+ if (!ASSERT_OK_PTR(nstoken, "open_netns"))
+ goto fail;
+ if (setup_netns())
+ goto fail;
+
+ veth1_idx = if_nametoindex("veth1");
+ veth2_idx = if_nametoindex("veth2");
+ if (!ASSERT_NEQ(veth1_idx, 0, "if_nametoindex(veth1)") ||
+ !ASSERT_NEQ(veth2_idx, 0, "if_nametoindex(veth2)"))
+ goto fail;
+
+ /*
+ * A redirect to veth1 is delivered to its peer veth2. veth_xdp_xmit()
+ * only accepts the frame if veth2's NAPI is up, which on veth means
+ * veth2 carries an XDP program; xdp_count tallies what arrives.
+ */
+ err = bpf_xdp_attach(veth2_idx, bpf_program__fd(skel->progs.xdp_count),
+ XDP_FLAGS_DRV_MODE, NULL);
+ if (!ASSERT_OK(err, "attach xdp_count on veth2"))
+ goto fail;
+ xdp_attached = true;
+
+ /* reducible VLAN egress: resolves to the physical parent veth1 */
+ memset(fib_params, 0, sizeof(*fib_params));
+ fib_params->family = AF_INET;
+ fib_params->l4_protocol = IPPROTO_TCP;
+ fib_params->ifindex = veth1_idx;
+ if (!ASSERT_EQ(inet_pton(AF_INET, IPV4_IFACE_ADDR, &fib_params->ipv4_src),
+ 1, "inet_pton(src)") ||
+ !ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_EGRESS_DST, &fib_params->ipv4_dst),
+ 1, "inet_pton(reducible dst)"))
+ goto fail;
+ skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH;
+ skel->bss->redirected = 0;
+ skel->bss->passed = 0;
+ skel->bss->delivered = 0;
+
+ err = bpf_prog_test_run_opts(redirect_fd, &lf_opts);
+ if (!ASSERT_OK(err, "test_run(reducible egress)"))
+ goto fail;
+ ASSERT_EQ(skel->bss->redirected, REDIRECT_NPKTS, "reducible egress redirected");
+ ASSERT_EQ(skel->bss->passed, 0, "reducible egress not passed");
+ ASSERT_GT(skel->bss->delivered, 0, "reducible egress delivered to veth2");
+
+ /*
+ * QinQ egress: not reducible, so the lookup returns VLAN_FAILURE and
+ * the program passes the frame instead of redirecting to the inner
+ * VLAN device. redirected == 0 is the assertion that matters: the
+ * program did not redirect to a device that would drop the frame at
+ * xdp_do_flush(). veth2's delivered count is not checked here, since
+ * a passed frame can still reach veth2 through the stack's forwarding
+ * path, which is unrelated to the redirect under test.
+ */
+ memset(fib_params, 0, sizeof(*fib_params));
+ fib_params->family = AF_INET;
+ fib_params->l4_protocol = IPPROTO_TCP;
+ fib_params->ifindex = veth1_idx;
+ if (!ASSERT_EQ(inet_pton(AF_INET, IPV4_IFACE_ADDR, &fib_params->ipv4_src),
+ 1, "inet_pton(src)") ||
+ !ASSERT_EQ(inet_pton(AF_INET, IPV4_QINQ_DST, &fib_params->ipv4_dst),
+ 1, "inet_pton(qinq dst)"))
+ goto fail;
+ skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH;
+ skel->bss->redirected = 0;
+ skel->bss->passed = 0;
+
+ err = bpf_prog_test_run_opts(redirect_fd, &lf_opts);
+ if (!ASSERT_OK(err, "test_run(qinq egress)"))
+ goto fail;
+ ASSERT_EQ(skel->bss->passed, REDIRECT_NPKTS, "qinq egress passed");
+ ASSERT_EQ(skel->bss->redirected, 0, "qinq egress not redirected");
+
fail:
+ if (xdp_attached)
+ bpf_xdp_detach(veth2_idx, XDP_FLAGS_DRV_MODE, NULL);
if (nstoken)
close_netns(nstoken);
SYS_NOFAIL("ip netns del " NS_TEST);
diff --git a/tools/testing/selftests/bpf/progs/fib_lookup.c b/tools/testing/selftests/bpf/progs/fib_lookup.c
index 7b5dd2214ff4..862a1e9457b4 100644
--- a/tools/testing/selftests/bpf/progs/fib_lookup.c
+++ b/tools/testing/selftests/bpf/progs/fib_lookup.c
@@ -19,4 +19,40 @@ int fib_lookup(struct __sk_buff *skb)
return TC_ACT_SHOT;
}
+SEC("xdp")
+int fib_lookup_xdp(struct xdp_md *ctx)
+{
+ fib_lookup_ret = bpf_fib_lookup(ctx, &fib_params, sizeof(fib_params),
+ lookup_flags);
+
+ return XDP_DROP;
+}
+
+int redirected = 0;
+int passed = 0;
+int delivered = 0;
+
+SEC("xdp")
+int fib_lookup_redirect(struct xdp_md *ctx)
+{
+ struct bpf_fib_lookup params = fib_params;
+ long ret;
+
+ ret = bpf_fib_lookup(ctx, ¶ms, sizeof(params), lookup_flags);
+ if (ret == BPF_FIB_LKUP_RET_SUCCESS) {
+ redirected++;
+ return bpf_redirect(params.ifindex, 0);
+ }
+
+ passed++;
+ return XDP_PASS;
+}
+
+SEC("xdp")
+int xdp_count(struct xdp_md *ctx)
+{
+ delivered++;
+ return XDP_DROP;
+}
+
char _license[] SEC("license") = "GPL";
--
2.54.0
^ permalink raw reply related
* Re: [PATCH net v2] ipvs: fix PMTU for GUE/GRE tunnel ICMP errors
From: Julian Anastasov @ 2026-07-04 10:03 UTC (permalink / raw)
To: Yizhou Zhao
Cc: netdev, Simon Horman, Pablo Neira Ayuso, Florian Westphal,
Phil Sutter, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, lvs-devel, netfilter-devel, coreteam, linux-kernel,
stable, Yuxiang Yang, Ao Wang, Xuewei Feng, Qi Li, Ke Xu
In-Reply-To: <20260702073430.67680-1-zhaoyz24@mails.tsinghua.edu.cn>
Hello,
On Thu, 2 Jul 2026, Yizhou Zhao wrote:
> When an ICMP Fragmentation Needed error is received for a tunneled IPVS
> connection, ip_vs_in_icmp() recomputes the MTU that the original packet
> can use by subtracting the tunnel overhead from the reported next-hop
> MTU.
>
> The current code always subtracts sizeof(struct iphdr), which is only
> the IPIP overhead. For GUE and GRE tunnels, ipvs_udp_decap() and
> ipvs_gre_decap() already compute the additional tunnel header length,
> but that value is scoped to the decapsulation block and is lost before
> the ICMP_FRAG_NEEDED handling. As a result, the ICMP error sent back to
> the client advertises an MTU that is too large, so PMTUD can fail to
> converge for GUE/GRE-tunneled real servers.
>
> With a reported next-hop MTU of 1400, a GUE tunnel currently returns
> 1380 to the client. The correct value is 1368:
>
> 1400 - sizeof(struct iphdr) - sizeof(struct udphdr) -
> sizeof(struct guehdr)
>
> Hoist the tunnel header length into the main ip_vs_in_icmp() scope and
> subtract sizeof(struct iphdr) + ulen in the Fragmentation Needed path.
> The IPIP path keeps ulen as 0, so its existing 1400 - 20 = 1380 result
> is unchanged.
>
> Fixes: 508f744c0de3 ("ipvs: strip udp tunnel headers from icmp errors")
> Cc: stable@vger.kernel.org
> Reported-by: Yizhou Zhao <zhaoyz24@mails.tsinghua.edu.cn>
> Reported-by: Yuxiang Yang <yangyx22@mails.tsinghua.edu.cn>
> Reported-by: Ao Wang <wangao@seu.edu.cn>
> Reported-by: Xuewei Feng <fengxw06@126.com>
> Reported-by: Qi Li <qli01@tsinghua.edu.cn>
> Reported-by: Ke Xu <xuke@tsinghua.edu.cn>
> Assisted-by: Claude-Code:GLM-5.2
> Signed-off-by: Yizhou Zhao <zhaoyz24@mails.tsinghua.edu.cn>
I'm late for this patch, in case there is another iteration:
Acked-by: Julian Anastasov <ja@ssi.bg>
BTW, I prepared followup patch to add the needed pskb_may_pull()
as suggested by Sashiko:
https://sashiko.dev/#/patchset/20260702073430.67680-1-zhaoyz24%40mails.tsinghua.edu.cn
I'll wait this patch to be applied before posting it...
> ---
> Changes in v2:
> - Use the short first hunk context so patch applies without fuzz.
> - Adjust Assisted-by to checkpatch's agent-name format.
> - Suggested by Julian.
> - Link to v1: https://lore.kernel.org/netdev/20260701065941.46249-1-zhaoyz24@mails.tsinghua.edu.cn/
> ---
> net/netfilter/ipvs/ip_vs_core.c | 5 +++--
> 1 file changed, 3 insertions(+), 2 deletions(-)
>
> diff --git a/net/netfilter/ipvs/ip_vs_core.c b/net/netfilter/ipvs/ip_vs_core.c
> index d40b404c1bf6..906f2c361676 100644
> --- a/net/netfilter/ipvs/ip_vs_core.c
> +++ b/net/netfilter/ipvs/ip_vs_core.c
> @@ -1767,6 +1767,7 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
> bool tunnel, new_cp = false;
> union nf_inet_addr *raddr;
> char *outer_proto = "IPIP";
> + int ulen = 0;
>
> *related = 1;
>
> @@ -1831,7 +1832,6 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
> /* Error for our tunnel must arrive at LOCAL_IN */
> (skb_rtable(skb)->rt_flags & RTCF_LOCAL)) {
> __u8 iproto;
> - int ulen;
>
> /* Non-first fragment has no UDP/GRE header */
> if (unlikely(cih->frag_off & htons(IP_OFFSET)))
> @@ -1936,8 +1936,8 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
> if (dest_dst)
> mtu = dst_mtu(dest_dst->dst_cache);
> }
> - if (mtu > 68 + sizeof(struct iphdr))
> - mtu -= sizeof(struct iphdr);
> + if (mtu > 68 + sizeof(struct iphdr) + ulen)
> + mtu -= sizeof(struct iphdr) + ulen;
> info = htonl(mtu);
> }
> /* Strip outer IP, ICMP and IPIP/UDP/GRE, go to IP header of
Regards
--
Julian Anastasov <ja@ssi.bg>
^ permalink raw reply
* Re: [PATCH net-next v8 2/3] net: airoha: fix ETS QoS stats counter underflow and cross-channel corruption
From: Lorenzo Bianconi @ 2026-07-04 10:45 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Alexander Lobakin, linux-arm-kernel, linux-mediatek,
netdev
In-Reply-To: <20260703-airoha-ethtool-priv_flags-v8-2-015ba5ac89ee@kernel.org>
[-- Attachment #1: Type: text/plain, Size: 4008 bytes --]
> airoha_qdma_get_tx_ets_stats() has two bugs:
> - The hardware counters read via airoha_qdma_rr() are 32-bit values
> but are stored in u64 locals and subtracted from u64 baselines. When
> a 32-bit hardware counter wraps around, the subtraction produces a
> large underflow value passed to _bstats_update().
> - The baseline counters (cpu_tx_packets, fwd_tx_packets) are stored as
> single per-device fields, but airoha_qdma_get_tx_ets_stats() is
> called with different channel values (0-3). Each call reads a
> different channel's hardware counter but overwrites the same
> baseline, corrupting the delta computation for other channels.
>
> Fix both by:
> - Narrowing the counter locals and baselines to u32 so that 32-bit
> unsigned subtraction handles wrap-around naturally.
> - Grouping the baselines into a per-channel qos_stats array so each
> channel tracks its own previous counter value independently.
> - Splitting the delta addition into two statements so the first u32
> delta is widened to u64 on assignment and the second is added in
> u64 arithmetic, preventing overflow when both deltas are large.
>
> Fixes: 20bf7d07c956 ("net: airoha: Add sched ETS offload support")
> Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
Commenting on sashiko's report:
https://sashiko.dev/#/patchset/20260703-airoha-ethtool-priv_flags-v8-0-015ba5ac89ee%40kernel.org
> ---
> drivers/net/ethernet/airoha/airoha_eth.c | 18 +++++++++++-------
> drivers/net/ethernet/airoha/airoha_eth.h | 7 ++++---
> 2 files changed, 15 insertions(+), 10 deletions(-)
>
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
> index 41c1a0ffbdd8..aaf2a4717d12 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.c
> +++ b/drivers/net/ethernet/airoha/airoha_eth.c
> @@ -2482,16 +2482,20 @@ static int airoha_qdma_get_tx_ets_stats(struct net_device *netdev, int channel,
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> struct airoha_qdma *qdma = dev->qdma;
> + u32 cpu_tx_packets, fwd_tx_packets;
> + u64 tx_packets;
>
> - u64 cpu_tx_packets = airoha_qdma_rr(qdma, REG_CNTR_VAL(channel << 1));
> - u64 fwd_tx_packets = airoha_qdma_rr(qdma,
> - REG_CNTR_VAL((channel << 1) + 1));
> - u64 tx_packets = (cpu_tx_packets - dev->cpu_tx_packets) +
> - (fwd_tx_packets - dev->fwd_tx_packets);
> + cpu_tx_packets = airoha_qdma_rr(qdma, REG_CNTR_VAL(channel << 1));
> + fwd_tx_packets = airoha_qdma_rr(qdma,
> + REG_CNTR_VAL((channel << 1) + 1));
> + tx_packets = (u32)(cpu_tx_packets -
> + dev->qos_stats[channel].cpu_tx_packets);
> + tx_packets += (u32)(fwd_tx_packets -
> + dev->qos_stats[channel].fwd_tx_packets);
>
> _bstats_update(opt->stats.bstats, 0, tx_packets);
> - dev->cpu_tx_packets = cpu_tx_packets;
> - dev->fwd_tx_packets = fwd_tx_packets;
> + dev->qos_stats[channel].cpu_tx_packets = cpu_tx_packets;
> + dev->qos_stats[channel].fwd_tx_packets = fwd_tx_packets;
>
> return 0;
- This is a pre-existing issue, but I noticed the channel parameter passed to
this function appears to be derived incorrectly in airoha_tc_setup_qdisc_ets().
- As pointed out by sashiko, this issue has not been introduced by this patch
and I will fix it with a dedicated patch.
Regards,
Lorenzo
> }
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
> index bf1c249255bd..bf44be9f0954 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.h
> +++ b/drivers/net/ethernet/airoha/airoha_eth.h
> @@ -553,9 +553,10 @@ struct airoha_gdm_dev {
> struct airoha_eth *eth;
>
> DECLARE_BITMAP(qos_sq_bmap, AIROHA_NUM_QOS_CHANNELS);
> - /* qos stats counters */
> - u64 cpu_tx_packets;
> - u64 fwd_tx_packets;
> + struct {
> + u32 cpu_tx_packets;
> + u32 fwd_tx_packets;
> + } qos_stats[AIROHA_NUM_QOS_CHANNELS];
>
> u32 flags;
> int nbq;
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* [PATCH net-next v6 0/4] net: dsa: mt7628 embedded switch initial support
From: Joris Vaisvila @ 2026-07-04 10:56 UTC (permalink / raw)
To: netdev
Cc: horms, pabeni, kuba, edumazet, davem, olteanv, Andrew Lunn,
devicetree, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Arınç ÜNAL, Landen Chao, DENG Qingfang, Sean Wang,
Daniel Golle, Joris Vaisvila
This patch series adds initial support for the MediaTek MT7628 Embedded
Switch.
The driver implements the basic functionality required to operate the
switch using DSA. The hardware provides five internal Fast Ethernet user
ports and one Gigabit port connected internally to the CPU MAC.
Bridge offloading is not yet supported.
Tested on an MT7628NN-based board.
Changes since v5:
- updated tag_mt7628 to kfree_skb() on error in receive path
- removed Reviewed-by tags from tag_mt7628
- removed mmio base pointer from mt7628_esw since it's only used to
initialize the regmap
- rebased on net-next
Changes since v4:
mt7628 dsa driver:
- updated mdiobus allocation to use ds->dev instead of esw->dev,
matching other DSA drivers (no functional change)
mt7628 phy driver:
- replaced phy_write() with phy_modify() when setting PHY init
bit (no functional change)
mt7628 dt binding:
- moved unevaluatedProperties after required block
- removed blank line between compatible and reg in example
Link: https://lore.kernel.org/netdev/20260608192948.289745-1-joey@tinyisr.com/t/#u
Changes since v3:
- rebased on latest net-next
mt7628 dsa driver:
- simplified vlan_add hardware vlan slot search
- fixed vlan_del not removing vid from port pvid
- separated mii_read/mii_write error handling from return
value parsing. Updated RD_DONE/WT_DONE bit checking
with clearer logic and a comment.
- moved NET_DSA_MT7628 after NET_DSA_MT7530 in Kconfig
- added missing reset return value checks in probe
- fixed mt7628_switch_ops missing const specifier
- removed mdio node parsing from of, as there is nothing
to configure
mt7628 dt binding:
- updated description to be more clear about port count
- dropped optional mdio subnode. the switch does not
expose an external MDIO bus and all integrated PHY
access is handled by the driver.
- removed unused switch0 label in example
Link: https://lore.kernel.org/netdev/20260428185510.261521-1-joey@tinyisr.com/t/#u
Changes since v2:
- fix binding issues found in review
- fix ignored dsa_tag_8021q_register return value
- add switch teardown to clean up tag_8021q
- fix ordering issue where mdio probe fail would leak tag_8021q
Link: https://lore.kernel.org/netdev/20260330184017.766200-1-joey@tinyisr.com/t/#u
Changes since v1:
- changed port 6 phy-mode to internal
- cleaned up tag_mt7628 rcv function and mask defines
- fixed sorting error in drivers/net/dsa/ Kconfig and Makefile
- fixed sorting error in net/dsa/ Kconfig and Makefile
- fixed mt7628_mii_read/write return values on error
Link: https://lore.kernel.org/netdev/20260326204413.3317584-1-joey@tinyisr.com/t/#u
Thanks,
Joris
Joris Vaisvila (4):
dt-bindings: net: dsa: add MT7628 ESW
net: phy: mediatek: add phy driver for MT7628 built-in Fast Ethernet
PHYs
net: dsa: initial MT7628 tagging driver
net: dsa: initial support for MT7628 embedded switch
.../bindings/net/dsa/mediatek,mt7628-esw.yaml | 96 +++
drivers/net/dsa/Kconfig | 8 +
drivers/net/dsa/Makefile | 1 +
drivers/net/dsa/mt7628.c | 649 ++++++++++++++++++
drivers/net/phy/mediatek/Kconfig | 10 +-
drivers/net/phy/mediatek/Makefile | 1 +
drivers/net/phy/mediatek/mtk-fe-soc.c | 50 ++
include/net/dsa.h | 2 +
net/dsa/Kconfig | 6 +
net/dsa/Makefile | 1 +
net/dsa/tag_mt7628.c | 93 +++
11 files changed, 916 insertions(+), 1 deletion(-)
create mode 100644 Documentation/devicetree/bindings/net/dsa/mediatek,mt7628-esw.yaml
create mode 100644 drivers/net/dsa/mt7628.c
create mode 100644 drivers/net/phy/mediatek/mtk-fe-soc.c
create mode 100644 net/dsa/tag_mt7628.c
--
2.54.0
^ permalink raw reply
* [PATCH net-next v6 1/4] dt-bindings: net: dsa: add MT7628 ESW
From: Joris Vaisvila @ 2026-07-04 10:56 UTC (permalink / raw)
To: netdev
Cc: horms, pabeni, kuba, edumazet, davem, olteanv, Andrew Lunn,
devicetree, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Arınç ÜNAL, Landen Chao, DENG Qingfang, Sean Wang,
Daniel Golle, Joris Vaisvila, Krzysztof Kozlowski
In-Reply-To: <20260704105659.140970-1-joey@tinyisr.com>
Add device tree bindings for the MediaTek MT7628 embedded Ethernet
Switch.
The Switch provides 5 external user ports and 1 internal CPU port, with
integrated 10/100 PHYs and fixed port to PHY mapping.
The CPU port is internally connected and uses port index 6.
Signed-off-by: Joris Vaisvila <joey@tinyisr.com>
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
---
.../bindings/net/dsa/mediatek,mt7628-esw.yaml | 96 +++++++++++++++++++
1 file changed, 96 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/dsa/mediatek,mt7628-esw.yaml
diff --git a/Documentation/devicetree/bindings/net/dsa/mediatek,mt7628-esw.yaml b/Documentation/devicetree/bindings/net/dsa/mediatek,mt7628-esw.yaml
new file mode 100644
index 000000000000..e0e7ffef6648
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/dsa/mediatek,mt7628-esw.yaml
@@ -0,0 +1,96 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/dsa/mediatek,mt7628-esw.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Mediatek MT7628 Embedded Ethernet Switch
+
+maintainers:
+ - Joris Vaisvila <joey@tinyisr.com>
+
+description:
+ The MT7628 SoC's built-in Ethernet Switch has five user ports and one
+ internally connected CPU port. The user ports are all connected to the SoC's
+ integrated Fast Ethernet PHYs. The switch registers are directly mapped in
+ the SoC's memory.
+
+allOf:
+ - $ref: dsa.yaml#/$defs/ethernet-ports
+
+properties:
+ compatible:
+ const: mediatek,mt7628-esw
+
+ reg:
+ maxItems: 1
+
+ resets:
+ items:
+ - description: internal switch block reset
+ - description: internal phy package reset
+
+ reset-names:
+ items:
+ - const: esw
+ - const: ephy
+
+required:
+ - compatible
+ - reg
+ - resets
+ - reset-names
+ - ethernet-ports
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ switch@10110000 {
+ compatible = "mediatek,mt7628-esw";
+ reg = <0x10110000 0x8000>;
+
+ resets = <&sysc 23>, <&sysc 24>;
+ reset-names = "esw", "ephy";
+
+ ethernet-ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-port@0 {
+ reg = <0>;
+ phy-mode = "internal";
+ };
+
+ ethernet-port@1 {
+ reg = <1>;
+ phy-mode = "internal";
+ };
+
+ ethernet-port@2 {
+ reg = <2>;
+ phy-mode = "internal";
+ };
+
+ ethernet-port@3 {
+ reg = <3>;
+ phy-mode = "internal";
+ };
+
+ ethernet-port@4 {
+ reg = <4>;
+ phy-mode = "internal";
+ };
+
+ ethernet-port@6 {
+ reg = <6>;
+ phy-mode = "internal";
+ ethernet = <ðernet>;
+
+ fixed-link {
+ speed = <1000>;
+ full-duplex;
+ };
+ };
+ };
+ };
--
2.54.0
^ permalink raw reply related
* [PATCH net-next v6 2/4] net: phy: mediatek: add phy driver for MT7628 built-in Fast Ethernet PHYs
From: Joris Vaisvila @ 2026-07-04 10:56 UTC (permalink / raw)
To: netdev
Cc: horms, pabeni, kuba, edumazet, davem, olteanv, Andrew Lunn,
devicetree, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Arınç ÜNAL, Landen Chao, DENG Qingfang, Sean Wang,
Daniel Golle, Joris Vaisvila
In-Reply-To: <20260704105659.140970-1-joey@tinyisr.com>
The Fast Ethernet PHYs present in the MT7628 SoCs require an
undocumented bit to be set before they can establish 100mbps links.
This commit adds the Kconfig option MEDIATEK_FE_SOC_PHY and the
corresponding driver mtk-fe-soc.c.
Signed-off-by: Joris Vaisvila <joey@tinyisr.com>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Reviewed-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/phy/mediatek/Kconfig | 10 +++++-
drivers/net/phy/mediatek/Makefile | 1 +
drivers/net/phy/mediatek/mtk-fe-soc.c | 50 +++++++++++++++++++++++++++
3 files changed, 60 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/phy/mediatek/mtk-fe-soc.c
diff --git a/drivers/net/phy/mediatek/Kconfig b/drivers/net/phy/mediatek/Kconfig
index bb7dc876271e..b6a51f38c358 100644
--- a/drivers/net/phy/mediatek/Kconfig
+++ b/drivers/net/phy/mediatek/Kconfig
@@ -21,8 +21,16 @@ config MEDIATEK_GE_PHY
common operations with MediaTek SoC built-in Gigabit
Ethernet PHYs.
+config MEDIATEK_FE_SOC_PHY
+ tristate "MediaTek SoC Fast Ethernet PHYs"
+ help
+ Support for MediaTek MT7628 built-in Fast Ethernet PHYs.
+ This driver only sets an initialization bit required for the PHY
+ to establish 100 Mbps links. All other PHY operations are handled
+ by the kernel's generic PHY code.
+
config MEDIATEK_GE_SOC_PHY
- tristate "MediaTek SoC Ethernet PHYs"
+ tristate "MediaTek SoC Gigabit Ethernet PHYs"
depends on ARM64 || COMPILE_TEST
depends on ARCH_AIROHA || (ARCH_MEDIATEK && NVMEM_MTK_EFUSE) || \
COMPILE_TEST
diff --git a/drivers/net/phy/mediatek/Makefile b/drivers/net/phy/mediatek/Makefile
index ac57ecc799fc..6f9cacf7f906 100644
--- a/drivers/net/phy/mediatek/Makefile
+++ b/drivers/net/phy/mediatek/Makefile
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_MEDIATEK_2P5GE_PHY) += mtk-2p5ge.o
+obj-$(CONFIG_MEDIATEK_FE_SOC_PHY) += mtk-fe-soc.o
obj-$(CONFIG_MEDIATEK_GE_PHY) += mtk-ge.o
obj-$(CONFIG_MEDIATEK_GE_SOC_PHY) += mtk-ge-soc.o
obj-$(CONFIG_MTK_NET_PHYLIB) += mtk-phy-lib.o
diff --git a/drivers/net/phy/mediatek/mtk-fe-soc.c b/drivers/net/phy/mediatek/mtk-fe-soc.c
new file mode 100644
index 000000000000..9eb4960bcaad
--- /dev/null
+++ b/drivers/net/phy/mediatek/mtk-fe-soc.c
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for MT7628 Embedded Switch internal Fast Ethernet PHYs
+ */
+#include <linux/module.h>
+#include <linux/phy.h>
+
+#define MTK_FPHY_ID_MT7628 0x03a29410
+#define MTK_EXT_PAGE_ACCESS 0x1f
+
+static int mt7628_phy_read_page(struct phy_device *phydev)
+{
+ return __phy_read(phydev, MTK_EXT_PAGE_ACCESS);
+}
+
+static int mt7628_phy_write_page(struct phy_device *phydev, int page)
+{
+ return __phy_write(phydev, MTK_EXT_PAGE_ACCESS, page);
+}
+
+static int mt7628_phy_config_init(struct phy_device *phydev)
+{
+ /*
+ * This undocumented bit is required for the PHYs to be able to
+ * establish 100mbps links.
+ */
+ return phy_modify_paged(phydev, 0x8000, 30, BIT(13), BIT(13));
+}
+
+static struct phy_driver mtk_soc_fe_phy_driver[] = {
+ {
+ PHY_ID_MATCH_EXACT(MTK_FPHY_ID_MT7628),
+ .name = "MediaTek MT7628 PHY",
+ .config_init = mt7628_phy_config_init,
+ .read_page = mt7628_phy_read_page,
+ .write_page = mt7628_phy_write_page,
+ },
+};
+
+module_phy_driver(mtk_soc_fe_phy_driver);
+static const struct mdio_device_id __maybe_unused mtk_soc_fe_phy_tbl[] = {
+ { PHY_ID_MATCH_EXACT(MTK_FPHY_ID_MT7628) },
+ { }
+};
+
+MODULE_DESCRIPTION("MediaTek SoC Fast Ethernet PHY driver");
+MODULE_AUTHOR("Joris Vaisvila <joey@tinyisr.com>");
+MODULE_LICENSE("GPL");
+
+MODULE_DEVICE_TABLE(mdio, mtk_soc_fe_phy_tbl);
--
2.54.0
^ permalink raw reply related
* [PATCH net-next v6 3/4] net: dsa: initial MT7628 tagging driver
From: Joris Vaisvila @ 2026-07-04 10:56 UTC (permalink / raw)
To: netdev
Cc: horms, pabeni, kuba, edumazet, davem, olteanv, Andrew Lunn,
devicetree, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Arınç ÜNAL, Landen Chao, DENG Qingfang, Sean Wang,
Daniel Golle, Joris Vaisvila
In-Reply-To: <20260704105659.140970-1-joey@tinyisr.com>
Add support for the MT7628 embedded switch's tag.
The MT7628 tag is merged with the VLAN TPID field when a VLAN is
appended by the switch hardware. It is not installed if the VLAN tag is
already there on ingress. Due to this hardware quirk the tag cannot be
trusted for port 0 if we don't know that the VLAN was added by the
hardware. As a workaround for this the switch is configured to always
append the port PVID tag even if the incoming packet is already tagged.
The tagging driver can then trust that the tag is always accurate and
the whole VLAN tag can be removed on ingress as it's only metadata for
the tagger.
On egress the MT7628 tag allows precise TX, but the correct VLAN tag
from tag_8021q is still appended or the switch will not forward the
packet.
Signed-off-by: Joris Vaisvila <joey@tinyisr.com>
---
include/net/dsa.h | 2 +
net/dsa/Kconfig | 6 +++
net/dsa/Makefile | 1 +
net/dsa/tag_mt7628.c | 93 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 102 insertions(+)
create mode 100644 net/dsa/tag_mt7628.c
diff --git a/include/net/dsa.h b/include/net/dsa.h
index 8c16ef23cc10..913d1f71e3db 100644
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -59,6 +59,7 @@ struct tc_action;
#define DSA_TAG_PROTO_MXL_GSW1XX_VALUE 31
#define DSA_TAG_PROTO_MXL862_VALUE 32
#define DSA_TAG_PROTO_NETC_VALUE 33
+#define DSA_TAG_PROTO_MT7628_VALUE 34
enum dsa_tag_protocol {
DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
@@ -95,6 +96,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_MXL_GSW1XX = DSA_TAG_PROTO_MXL_GSW1XX_VALUE,
DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
DSA_TAG_PROTO_NETC = DSA_TAG_PROTO_NETC_VALUE,
+ DSA_TAG_PROTO_MT7628 = DSA_TAG_PROTO_MT7628_VALUE,
};
struct dsa_switch;
diff --git a/net/dsa/Kconfig b/net/dsa/Kconfig
index d5e725b90d78..23b4b74004ed 100644
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -98,6 +98,12 @@ config NET_DSA_TAG_EDSA
Say Y or M if you want to enable support for tagging frames for the
Marvell switches which use EtherType DSA headers.
+config NET_DSA_TAG_MT7628
+ tristate "Tag driver for the MT7628 embedded switch"
+ help
+ Say Y or M if you want to enable support for tagging frames for the
+ switch embedded in the MT7628 SoC.
+
config NET_DSA_TAG_MTK
tristate "Tag driver for Mediatek switches"
help
diff --git a/net/dsa/Makefile b/net/dsa/Makefile
index b8c2667cd14a..d15bcf5c68f0 100644
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -27,6 +27,7 @@ obj-$(CONFIG_NET_DSA_TAG_GSWIP) += tag_gswip.o
obj-$(CONFIG_NET_DSA_TAG_HELLCREEK) += tag_hellcreek.o
obj-$(CONFIG_NET_DSA_TAG_KSZ) += tag_ksz.o
obj-$(CONFIG_NET_DSA_TAG_LAN9303) += tag_lan9303.o
+obj-$(CONFIG_NET_DSA_TAG_MT7628) += tag_mt7628.o
obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk.o
obj-$(CONFIG_NET_DSA_TAG_MXL_862XX) += tag_mxl862xx.o
obj-$(CONFIG_NET_DSA_TAG_MXL_GSW1XX) += tag_mxl-gsw1xx.o
diff --git a/net/dsa/tag_mt7628.c b/net/dsa/tag_mt7628.c
new file mode 100644
index 000000000000..80b50ff08e53
--- /dev/null
+++ b/net/dsa/tag_mt7628.c
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026, Joris Vaisvila <joey@tinyisr.com>
+ * MT7628 switch tag support
+ */
+
+#include <linux/etherdevice.h>
+#include <linux/dsa/8021q.h>
+#include <net/dsa.h>
+
+#include "tag.h"
+
+/*
+ * The MT7628 tag is encoded in the VLAN TPID field.
+ * On TX the lower 6 bits encode the destination port bitmask.
+ * On RX the lower 3 bits encode the source port number.
+ *
+ * The switch hardware will not modify the TPID of an incoming packet if it is
+ * already VLAN tagged. To work around this the switch is configured to always
+ * append a tag_8021q standalone VLAN tag for each port. That means we can
+ * safely strip the outer VLAN tag after parsing it.
+ *
+ * A VLAN tag is constructed on egress to target the standalone VLAN and
+ * destination port.
+ */
+
+#define MT7628_TAG_NAME "mt7628"
+
+#define MT7628_TAG_TX_PORT GENMASK(5, 0)
+#define MT7628_TAG_RX_PORT GENMASK(2, 0)
+#define MT7628_TAG_LEN 4
+
+static struct sk_buff *mt7628_tag_xmit(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ struct dsa_port *dp;
+ u16 xmit_vlan;
+ __be16 *tag;
+
+ dp = dsa_user_to_port(dev);
+ xmit_vlan = dsa_tag_8021q_standalone_vid(dp);
+
+ skb_push(skb, MT7628_TAG_LEN);
+ dsa_alloc_etype_header(skb, MT7628_TAG_LEN);
+
+ tag = dsa_etype_header_pos_tx(skb);
+
+ tag[0] = htons(ETH_P_8021Q |
+ FIELD_PREP(MT7628_TAG_TX_PORT,
+ dsa_xmit_port_mask(skb, dev)));
+ tag[1] = htons(xmit_vlan);
+
+ return skb;
+}
+
+static struct sk_buff *mt7628_tag_rcv(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ __be16 *phdr;
+
+ if (unlikely(!pskb_may_pull(skb, MT7628_TAG_LEN))) {
+ kfree_skb(skb);
+ return NULL;
+ }
+
+ phdr = dsa_etype_header_pos_rx(skb);
+ skb->dev =
+ dsa_conduit_find_user(dev, 0,
+ FIELD_GET(MT7628_TAG_RX_PORT, ntohs(*phdr)));
+ if (!skb->dev) {
+ kfree_skb(skb);
+ return NULL;
+ }
+
+ skb_pull_rcsum(skb, MT7628_TAG_LEN);
+ dsa_strip_etype_header(skb, MT7628_TAG_LEN);
+ dsa_default_offload_fwd_mark(skb);
+ return skb;
+}
+
+static const struct dsa_device_ops mt7628_tag_ops = {
+ .name = MT7628_TAG_NAME,
+ .proto = DSA_TAG_PROTO_MT7628,
+ .xmit = mt7628_tag_xmit,
+ .rcv = mt7628_tag_rcv,
+ .needed_headroom = MT7628_TAG_LEN,
+};
+
+module_dsa_tag_driver(mt7628_tag_ops);
+
+MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_MT7628, MT7628_TAG_NAME);
+MODULE_DESCRIPTION("DSA tag driver for MT7628 switch");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related
* [PATCH net-next v6 4/4] net: dsa: initial support for MT7628 embedded switch
From: Joris Vaisvila @ 2026-07-04 10:56 UTC (permalink / raw)
To: netdev
Cc: horms, pabeni, kuba, edumazet, davem, olteanv, Andrew Lunn,
devicetree, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Arınç ÜNAL, Landen Chao, DENG Qingfang, Sean Wang,
Daniel Golle, Joris Vaisvila
In-Reply-To: <20260704105659.140970-1-joey@tinyisr.com>
Add support for the MT7628 embedded switch.
The switch has 5 built-in 100Mbps user ports (ports 0-4) and one 1Gbps
port that is internally attached to the SoCs CPU MAC and serves as the
CPU port.
The switch hardware has a very limited 16 entry VLAN table. Configuring
VLANs is the only way to control switch forwarding. Currently 6 entries
are used by tag_8021q to isolate the ports. Double tag feature is
enabled to force the switch to append the VLAN tag even if the incoming
packet is already tagged, this simulates VLAN-unaware functionality and
simplifies the tagger implementation.
Signed-off-by: Joris Vaisvila <joey@tinyisr.com>
Reviewed-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/Kconfig | 8 +
drivers/net/dsa/Makefile | 1 +
drivers/net/dsa/mt7628.c | 649 +++++++++++++++++++++++++++++++++++++++
3 files changed, 658 insertions(+)
create mode 100644 drivers/net/dsa/mt7628.c
diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
index 4ab567c5bbaf..daa1d3d4ba60 100644
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -63,6 +63,14 @@ config NET_DSA_MT7530_MMIO
are directly mapped into the SoCs register space rather than being
accessible via MDIO.
+config NET_DSA_MT7628
+ tristate "MediaTek MT7628 Embedded Ethernet switch support"
+ select NET_DSA_TAG_MT7628
+ select MEDIATEK_FE_SOC_PHY
+ help
+ This enables support for the built-in Ethernet switch found
+ in the MT7628 SoC.
+
config NET_DSA_MV88E6060
tristate "Marvell 88E6060 ethernet switch chip support"
select NET_DSA_TAG_TRAILER
diff --git a/drivers/net/dsa/Makefile b/drivers/net/dsa/Makefile
index d2975badffc0..6ceb78a755d7 100644
--- a/drivers/net/dsa/Makefile
+++ b/drivers/net/dsa/Makefile
@@ -6,6 +6,7 @@ obj-$(CONFIG_NET_DSA_KS8995) += ks8995.o
obj-$(CONFIG_NET_DSA_MT7530) += mt7530.o
obj-$(CONFIG_NET_DSA_MT7530_MDIO) += mt7530-mdio.o
obj-$(CONFIG_NET_DSA_MT7530_MMIO) += mt7530-mmio.o
+obj-$(CONFIG_NET_DSA_MT7628) += mt7628.o
obj-$(CONFIG_NET_DSA_MV88E6060) += mv88e6060.o
obj-$(CONFIG_NET_DSA_RZN1_A5PSW) += rzn1_a5psw.o
obj-$(CONFIG_NET_DSA_SMSC_LAN9303) += lan9303-core.o
diff --git a/drivers/net/dsa/mt7628.c b/drivers/net/dsa/mt7628.c
new file mode 100644
index 000000000000..eccdb05fce9b
--- /dev/null
+++ b/drivers/net/dsa/mt7628.c
@@ -0,0 +1,649 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Mediatek MT7628 Embedded Switch (ESW) DSA driver
+ * Copyright (C) 2026 Joris Vaisvila <joey@tinyisr.com>
+ *
+ * Portions derived from OpenWRT esw_rt3050 driver:
+ * Copyright (C) 2009-2015 John Crispin <blogic@openwrt.org>
+ * Copyright (C) 2009-2015 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2013-2015 Michael Lee <igvtee@gmail.com>
+ * Copyright (C) 2016 Vittorio Gambaletta <openwrt@vittgam.net>
+ */
+
+#include <linux/platform_device.h>
+#include <linux/etherdevice.h>
+#include <linux/netdevice.h>
+#include <linux/dsa/8021q.h>
+#include <linux/if_bridge.h>
+#include <linux/module.h>
+#include <linux/mdio.h>
+#include <linux/of.h>
+#include <linux/of_mdio.h>
+#include <linux/of_net.h>
+#include <linux/kernel.h>
+#include <linux/regmap.h>
+#include <linux/reset.h>
+#include <net/dsa.h>
+
+#define MT7628_ESW_REG_IMR 0x04
+#define MT7628_ESW_REG_FCT0 0x08
+#define MT7628_ESW_REG_PFC1 0x14
+#define MT7628_ESW_REG_PVIDC(port) (0x40 + 4 * ((port) / 2))
+#define MT7628_ESW_REG_VLANI(vlan) (0x50 + 4 * ((vlan) / 2))
+#define MT7628_ESW_REG_VMSC(vlan) (0x70 + 4 * ((vlan) / 4))
+#define MT7628_ESW_REG_VUB(vlan) (0x100 + 4 * ((vlan) / 4))
+#define MT7628_ESW_REG_SOCPC 0x8c
+#define MT7628_ESW_REG_POC0 0x90
+#define MT7628_ESW_REG_POC2 0x98
+#define MT7628_ESW_REG_SGC 0x9c
+#define MT7628_ESW_REG_PCR0 0xc0
+#define MT7628_ESW_REG_PCR1 0xc4
+#define MT7628_ESW_REG_FPA2 0xc8
+#define MT7628_ESW_REG_FCT2 0xcc
+#define MT7628_ESW_REG_SGC2 0xe4
+
+#define MT7628_ESW_FCT0_DROP_SET_TH GENMASK(7, 0)
+#define MT7628_ESW_FCT0_DROP_RLS_TH GENMASK(15, 8)
+#define MT7628_ESW_FCT0_FC_SET_TH GENMASK(23, 16)
+#define MT7628_ESW_FCT0_FC_RLS_TH GENMASK(31, 24)
+
+#define MT7628_ESW_PFC1_EN_VLAN GENMASK(22, 16)
+
+#define MT7628_ESW_PVID_S 12
+#define MT7628_ESW_PVID_M GENMASK(11, 0)
+#define MT7628_ESW_PVID_SHIFT(port) \
+ (MT7628_ESW_PVID_S * ((port) % 2))
+#define MT7628_ESW_PVID_MASK(port) \
+ (MT7628_ESW_PVID_M << MT7628_ESW_PVID_SHIFT(port))
+#define MT7628_ESW_PVID_PREP(port, pvid) \
+ (((pvid) & MT7628_ESW_PVID_M) << MT7628_ESW_PVID_SHIFT(port))
+
+#define MT7628_ESW_VID_S 12
+#define MT7628_ESW_VID_M GENMASK(11, 0)
+#define MT7628_ESW_VID_SHIFT(vlan) \
+ (MT7628_ESW_VID_S * ((vlan) % 2))
+#define MT7628_ESW_VID_MASK(vlan) \
+ (MT7628_ESW_VID_M << MT7628_ESW_VID_SHIFT(vlan))
+#define MT7628_ESW_VID_PREP(vlan, vid) \
+ (((vid) & MT7628_ESW_VID_M) << MT7628_ESW_VID_SHIFT(vlan))
+
+#define MT7628_ESW_VMSC_S 8
+#define MT7628_ESW_VMSC_M GENMASK(7, 0)
+#define MT7628_ESW_VMSC_SHIFT(vlan) \
+ (MT7628_ESW_VMSC_S * ((vlan) % 4))
+#define MT7628_ESW_VMSC_MASK(vlan) \
+ (MT7628_ESW_VMSC_M << MT7628_ESW_VMSC_SHIFT(vlan))
+#define MT7628_ESW_VMSC_PREP(vlan, vmsc) \
+ (((vmsc) & MT7628_ESW_VMSC_M) << MT7628_ESW_VMSC_SHIFT(vlan))
+
+#define MT7628_ESW_VUB_S 7
+#define MT7628_ESW_VUB_M GENMASK(6, 0)
+#define MT7628_ESW_VUB_SHIFT(vlan) \
+ (MT7628_ESW_VUB_S * ((vlan) % 4))
+#define MT7628_ESW_VUB_MASK(vlan) \
+ (MT7628_ESW_VUB_M << MT7628_ESW_VUB_SHIFT(vlan))
+#define MT7628_ESW_VUB_PREP(vlan, vub) \
+ (((vub) & MT7628_ESW_VUB_M) << MT7628_ESW_VUB_SHIFT(vlan))
+
+#define MT7628_ESW_SOCPC_CRC_PADDING BIT(25)
+#define MT7628_ESW_SOCPC_DISBC2CPU GENMASK(22, 16)
+#define MT7628_ESW_SOCPC_DISMC2CPU GENMASK(14, 8)
+#define MT7628_ESW_SOCPC_DISUN2CPU GENMASK(6, 0)
+
+#define MT7628_ESW_POC0_PORT_DISABLE GENMASK(29, 23)
+
+#define MT7628_ESW_POC2_PER_VLAN_UNTAG_EN BIT(15)
+
+#define MT7628_ESW_SGC_AGING_INTERVAL GENMASK(3, 0)
+#define MT7628_ESW_BC_STORM_PROT GENMASK(5, 4)
+#define MT7628_ESW_PKT_MAX_LEN GENMASK(7, 6)
+#define MT7628_ESW_DIS_PKT_ABORT BIT(8)
+#define MT7628_ESW_ADDRESS_HASH_ALG GENMASK(10, 9)
+#define MT7628_ESW_DISABLE_TX_BACKOFF BIT(11)
+#define MT7628_ESW_BP_JAM_CNT GENMASK(15, 12)
+#define MT7628_ESW_DISMIIPORT_WASTX GENMASK(17, 16)
+#define MT7628_ESW_BP_MODE GENMASK(19, 18)
+#define MT7628_ESW_BISH_DIS BIT(20)
+#define MT7628_ESW_BISH_TH GENMASK(22, 21)
+#define MT7628_ESW_LED_FLASH_TIME GENMASK(24, 23)
+#define MT7628_ESW_RMC_RULE GENMASK(26, 25)
+#define MT7628_ESW_IP_MULT_RULE GENMASK(28, 27)
+#define MT7628_ESW_LEN_ERR_CHK BIT(29)
+#define MT7628_ESW_BKOFF_ALG BIT(30)
+
+#define MT7628_ESW_PCR0_WT_NWAY_DATA GENMASK(31, 16)
+#define MT7628_ESW_PCR0_RD_PHY_CMD BIT(14)
+#define MT7628_ESW_PCR0_WT_PHY_CMD BIT(13)
+#define MT7628_ESW_PCR0_CPU_PHY_REG GENMASK(12, 8)
+#define MT7628_ESW_PCR0_CPU_PHY_ADDR GENMASK(4, 0)
+
+#define MT7628_ESW_PCR1_RD_DATA GENMASK(31, 16)
+#define MT7628_ESW_PCR1_RD_DONE BIT(1)
+#define MT7628_ESW_PCR1_WT_DONE BIT(0)
+
+#define MT7628_ESW_FPA2_AP_EN BIT(29)
+#define MT7628_ESW_FPA2_EXT_PHY_ADDR_BASE GENMASK(28, 24)
+#define MT7628_ESW_FPA2_FORCE_RGMII_LINK1 BIT(13)
+#define MT7628_ESW_FPA2_FORCE_RGMII_EN1 BIT(11)
+
+#define MT7628_ESW_FCT2_MUST_DROP_RLS_TH GENMASK(17, 13)
+#define MT7628_ESW_FCT2_MUST_DROP_SET_TH GENMASK(12, 8)
+#define MT7628_ESW_FCT2_MC_PER_PORT_TH GENMASK(5, 0)
+
+#define MT7628_ESW_SGC2_SPECIAL_TAG_EN BIT(23)
+#define MT7628_ESW_SGC2_TX_CPU_TPID_BIT_MAP GENMASK(22, 16)
+#define MT7628_ESW_SGC2_DOUBLE_TAG_EN GENMASK(6, 0)
+
+#define MT7628_ESW_PORTS_NOCPU GENMASK(5, 0)
+#define MT7628_ESW_PORTS_CPU BIT(6)
+#define MT7628_ESW_PORTS_ALL GENMASK(6, 0)
+
+#define MT7628_ESW_NUM_PORTS 7
+#define MT7628_NUM_VLANS 16
+
+static const struct regmap_config mt7628_esw_regmap_cfg = {
+ .name = "mt7628-esw",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .fast_io = true,
+ .reg_format_endian = REGMAP_ENDIAN_LITTLE,
+ .val_format_endian = REGMAP_ENDIAN_LITTLE,
+};
+
+struct mt7628_vlan {
+ bool active;
+ u8 members;
+ u8 untag;
+ u16 vid;
+};
+
+struct mt7628_esw {
+ struct reset_control *rst_ephy;
+ struct reset_control *rst_esw;
+ struct regmap *regmap;
+ struct dsa_switch *ds;
+ u16 tag_8021q_pvid[MT7628_ESW_NUM_PORTS];
+ struct mt7628_vlan vlans[MT7628_NUM_VLANS];
+ struct device *dev;
+};
+
+static int mt7628_mii_read(struct mii_bus *bus, int port, int regnum)
+{
+ struct mt7628_esw *esw = bus->priv;
+ int ret;
+ u32 val;
+
+ /*
+ * RD_DONE bit is read to clear. Read PCR1 once to acknowledge any
+ * stale completion indicator before starting a new transaction.
+ */
+ ret = regmap_read(esw->regmap, MT7628_ESW_REG_PCR1, &val);
+ if (ret)
+ goto out;
+
+ ret = regmap_write(esw->regmap, MT7628_ESW_REG_PCR0,
+ FIELD_PREP(MT7628_ESW_PCR0_CPU_PHY_REG,
+ regnum) |
+ FIELD_PREP(MT7628_ESW_PCR0_CPU_PHY_ADDR,
+ port) | MT7628_ESW_PCR0_RD_PHY_CMD);
+ if (ret)
+ goto out;
+
+ ret = regmap_read_poll_timeout(esw->regmap, MT7628_ESW_REG_PCR1, val,
+ (val & MT7628_ESW_PCR1_RD_DONE), 10,
+ 5000);
+ if (ret)
+ goto out;
+
+ return FIELD_GET(MT7628_ESW_PCR1_RD_DATA, val);
+
+out:
+ dev_err(&bus->dev, "read failed. MDIO timeout?\n");
+ return ret;
+}
+
+static int mt7628_mii_write(struct mii_bus *bus, int port, int regnum, u16 dat)
+{
+ struct mt7628_esw *esw = bus->priv;
+ u32 val;
+ int ret;
+
+ /*
+ * WT_DONE bit is read to clear. Read PCR1 once to acknowledge any
+ * stale completion indicator before starting a new transaction.
+ */
+ ret = regmap_read(esw->regmap, MT7628_ESW_REG_PCR1, &val);
+ if (ret)
+ goto out;
+
+ ret = regmap_write(esw->regmap, MT7628_ESW_REG_PCR0,
+ FIELD_PREP(MT7628_ESW_PCR0_WT_NWAY_DATA, dat) |
+ FIELD_PREP(MT7628_ESW_PCR0_CPU_PHY_REG,
+ regnum) |
+ FIELD_PREP(MT7628_ESW_PCR0_CPU_PHY_ADDR,
+ port) | MT7628_ESW_PCR0_WT_PHY_CMD);
+ if (ret)
+ goto out;
+
+ ret = regmap_read_poll_timeout(esw->regmap, MT7628_ESW_REG_PCR1, val,
+ (val & MT7628_ESW_PCR1_WT_DONE), 10,
+ 5000);
+ if (ret)
+ goto out;
+
+ return 0;
+
+out:
+ dev_err(&bus->dev, "write failed. MDIO timeout?\n");
+ return ret;
+}
+
+static int mt7628_setup_internal_mdio(struct dsa_switch *ds)
+{
+ struct mt7628_esw *esw = ds->priv;
+ struct device *dev = ds->dev;
+ struct mii_bus *bus;
+
+ bus = devm_mdiobus_alloc(dev);
+ if (!bus)
+ return -ENOMEM;
+
+ bus->name = "MT7628 internal MDIO bus";
+ snprintf(bus->id, MII_BUS_ID_SIZE, "%s-mii", dev_name(dev));
+ bus->priv = esw;
+ bus->read = mt7628_mii_read;
+ bus->write = mt7628_mii_write;
+ bus->parent = dev;
+
+ ds->user_mii_bus = bus;
+ bus->phy_mask = ~ds->phys_mii_mask;
+
+ return devm_mdiobus_register(dev, bus);
+}
+
+static void mt7628_switch_init(struct dsa_switch *ds)
+{
+ struct mt7628_esw *esw = ds->priv;
+
+ regmap_write(esw->regmap, MT7628_ESW_REG_FCT0,
+ FIELD_PREP(MT7628_ESW_FCT0_DROP_SET_TH, 0x50) |
+ FIELD_PREP(MT7628_ESW_FCT0_DROP_RLS_TH, 0x78) |
+ FIELD_PREP(MT7628_ESW_FCT0_FC_SET_TH, 0xa0) |
+ FIELD_PREP(MT7628_ESW_FCT0_FC_RLS_TH, 0xc8));
+
+ regmap_write(esw->regmap, MT7628_ESW_REG_FCT2,
+ FIELD_PREP(MT7628_ESW_FCT2_MC_PER_PORT_TH, 0xc) |
+ FIELD_PREP(MT7628_ESW_FCT2_MUST_DROP_SET_TH, 0x10) |
+ FIELD_PREP(MT7628_ESW_FCT2_MUST_DROP_RLS_TH, 0x12));
+
+ /*
+ * general switch configuration:
+ * 300s aging interval
+ * broadcast storm prevention disabled
+ * max packet length 1536 bytes
+ * disable collision 16 packet abort and late collision abort
+ * use xor48 for address hashing
+ * disable tx backoff
+ * 10 packet back pressure jam
+ * disable was_transmit
+ * jam until BP condition released
+ * 30ms LED flash
+ * rmc tb fault to all ports
+ * unmatched IGMP as broadcast
+ */
+ regmap_write(esw->regmap, MT7628_ESW_REG_SGC,
+ FIELD_PREP(MT7628_ESW_SGC_AGING_INTERVAL, 1) |
+ FIELD_PREP(MT7628_ESW_BC_STORM_PROT, 0) |
+ FIELD_PREP(MT7628_ESW_PKT_MAX_LEN, 0) |
+ MT7628_ESW_DIS_PKT_ABORT |
+ FIELD_PREP(MT7628_ESW_ADDRESS_HASH_ALG, 1) |
+ MT7628_ESW_DISABLE_TX_BACKOFF |
+ FIELD_PREP(MT7628_ESW_BP_JAM_CNT, 10) |
+ FIELD_PREP(MT7628_ESW_DISMIIPORT_WASTX, 0) |
+ FIELD_PREP(MT7628_ESW_BP_MODE, 0b10) |
+ FIELD_PREP(MT7628_ESW_LED_FLASH_TIME, 0) |
+ FIELD_PREP(MT7628_ESW_RMC_RULE, 0) |
+ FIELD_PREP(MT7628_ESW_IP_MULT_RULE, 0));
+
+ regmap_write(esw->regmap, MT7628_ESW_REG_SOCPC,
+ MT7628_ESW_SOCPC_CRC_PADDING |
+ FIELD_PREP(MT7628_ESW_SOCPC_DISUN2CPU,
+ MT7628_ESW_PORTS_CPU) |
+ FIELD_PREP(MT7628_ESW_SOCPC_DISMC2CPU,
+ MT7628_ESW_PORTS_CPU) |
+ FIELD_PREP(MT7628_ESW_SOCPC_DISBC2CPU,
+ MT7628_ESW_PORTS_CPU));
+
+ regmap_set_bits(esw->regmap, MT7628_ESW_REG_FPA2,
+ MT7628_ESW_FPA2_FORCE_RGMII_EN1 |
+ MT7628_ESW_FPA2_FORCE_RGMII_LINK1 |
+ MT7628_ESW_FPA2_AP_EN);
+
+ regmap_update_bits(esw->regmap, MT7628_ESW_REG_FPA2,
+ MT7628_ESW_FPA2_EXT_PHY_ADDR_BASE,
+ FIELD_PREP(MT7628_ESW_FPA2_EXT_PHY_ADDR_BASE, 31));
+
+ /* disable all interrupts */
+ regmap_write(esw->regmap, MT7628_ESW_REG_IMR, 0);
+
+ /* enable MT7628 DSA tag on CPU port */
+ regmap_write(esw->regmap, MT7628_ESW_REG_SGC2,
+ MT7628_ESW_SGC2_SPECIAL_TAG_EN |
+ FIELD_PREP(MT7628_ESW_SGC2_TX_CPU_TPID_BIT_MAP,
+ MT7628_ESW_PORTS_CPU));
+
+ /*
+ * Double tag feature allows switch to always append the port PVID VLAN tag
+ * regardless of if the incoming packet already has a VLAN tag.
+ * This is enabled to simulate VLAN unawareness.
+ */
+ regmap_set_bits(esw->regmap, MT7628_ESW_REG_SGC2,
+ FIELD_PREP(MT7628_ESW_SGC2_DOUBLE_TAG_EN,
+ MT7628_ESW_PORTS_NOCPU));
+
+ regmap_set_bits(esw->regmap, MT7628_ESW_REG_POC2,
+ MT7628_ESW_POC2_PER_VLAN_UNTAG_EN);
+
+ regmap_update_bits(esw->regmap, MT7628_ESW_REG_PFC1,
+ MT7628_ESW_PFC1_EN_VLAN,
+ FIELD_PREP(MT7628_ESW_PFC1_EN_VLAN,
+ MT7628_ESW_PORTS_ALL));
+}
+
+static void mt7628_esw_set_pvid(struct mt7628_esw *esw, unsigned int port,
+ unsigned int pvid)
+{
+ regmap_update_bits(esw->regmap, MT7628_ESW_REG_PVIDC(port),
+ MT7628_ESW_PVID_MASK(port),
+ MT7628_ESW_PVID_PREP(port, pvid));
+}
+
+static void mt7628_esw_set_vlan_id(struct mt7628_esw *esw, unsigned int vlan,
+ unsigned int vid)
+{
+ regmap_update_bits(esw->regmap, MT7628_ESW_REG_VLANI(vlan),
+ MT7628_ESW_VID_MASK(vlan),
+ MT7628_ESW_VID_PREP(vlan, vid));
+}
+
+static void mt7628_esw_set_vmsc(struct mt7628_esw *esw, unsigned int vlan,
+ unsigned int msc)
+{
+ regmap_update_bits(esw->regmap, MT7628_ESW_REG_VMSC(vlan),
+ MT7628_ESW_VMSC_MASK(vlan),
+ MT7628_ESW_VMSC_PREP(vlan, msc));
+}
+
+static void mt7628_esw_set_vub(struct mt7628_esw *esw, unsigned int vlan,
+ unsigned int vub)
+{
+ regmap_update_bits(esw->regmap, MT7628_ESW_REG_VUB(vlan),
+ MT7628_ESW_VUB_MASK(vlan),
+ MT7628_ESW_VUB_PREP(vlan, vub));
+}
+
+static void mt7628_vlan_sync(struct dsa_switch *ds)
+{
+ struct mt7628_esw *esw = ds->priv;
+ int i;
+
+ for (i = 0; i < MT7628_NUM_VLANS; i++) {
+ struct mt7628_vlan *vlan = &esw->vlans[i];
+
+ mt7628_esw_set_vmsc(esw, i, vlan->members);
+ mt7628_esw_set_vlan_id(esw, i, vlan->vid);
+ mt7628_esw_set_vub(esw, i, vlan->untag);
+ }
+
+ for (i = 0; i < ds->num_ports; i++)
+ mt7628_esw_set_pvid(esw, i, esw->tag_8021q_pvid[i]);
+}
+
+static int mt7628_setup(struct dsa_switch *ds)
+{
+ struct mt7628_esw *esw = ds->priv;
+ int ret;
+
+ ret = reset_control_reset(esw->rst_esw);
+ if (ret)
+ return ret;
+ usleep_range(1000, 2000);
+
+ ret = reset_control_reset(esw->rst_ephy);
+ if (ret)
+ return ret;
+ usleep_range(1000, 2000);
+ /*
+ * all MMIO reads hang if esw is not out of reset
+ * ephy needs extra time to get out of reset or it ends up misconfigured
+ */
+
+ mt7628_switch_init(ds);
+
+ ret = mt7628_setup_internal_mdio(ds);
+ if (ret)
+ return ret;
+
+ rtnl_lock();
+ ret = dsa_tag_8021q_register(ds, htons(ETH_P_8021Q));
+ rtnl_unlock();
+
+ return ret;
+}
+
+static int mt7628_port_enable(struct dsa_switch *ds, int port,
+ struct phy_device *phy)
+{
+ struct mt7628_esw *esw = ds->priv;
+
+ regmap_clear_bits(esw->regmap, MT7628_ESW_REG_POC0,
+ FIELD_PREP(MT7628_ESW_POC0_PORT_DISABLE, BIT(port)));
+ return 0;
+}
+
+static void mt7628_port_disable(struct dsa_switch *ds, int port)
+{
+ struct mt7628_esw *esw = ds->priv;
+
+ regmap_set_bits(esw->regmap, MT7628_ESW_REG_POC0,
+ FIELD_PREP(MT7628_ESW_POC0_PORT_DISABLE, BIT(port)));
+}
+
+static enum dsa_tag_protocol
+mt7628_get_tag_proto(struct dsa_switch *ds, int port, enum dsa_tag_protocol mp)
+{
+ return DSA_TAG_PROTO_MT7628;
+}
+
+static void mt7628_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config)
+{
+ switch (port) {
+ case 6:
+ config->mac_capabilities |= MAC_1000;
+ fallthrough;
+ case 0 ... 4:
+ config->mac_capabilities |= MAC_100 | MAC_10;
+ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+ config->supported_interfaces);
+ break;
+ default:
+ break; /* port 5 does not exist on MT7628 */
+ }
+}
+
+static int mt7628_dsa_8021q_vlan_add(struct dsa_switch *ds, int port,
+ u16 vid, u16 flags)
+{
+ struct mt7628_esw *esw = ds->priv;
+ struct mt7628_vlan *vlan = NULL;
+ int i;
+
+ for (i = 0; i < MT7628_NUM_VLANS; i++) {
+ struct mt7628_vlan *check_vlan = &esw->vlans[i];
+
+ if (!check_vlan->active && !vlan)
+ vlan = check_vlan;
+
+ if (check_vlan->active && check_vlan->vid == vid) {
+ vlan = check_vlan;
+ break;
+ }
+ }
+
+ if (!vlan)
+ return -ENOSPC;
+
+ vlan->vid = vid;
+ vlan->active = true;
+ vlan->members |= BIT(port);
+
+ if (flags & BRIDGE_VLAN_INFO_PVID)
+ esw->tag_8021q_pvid[port] = vid;
+
+ if (flags & BRIDGE_VLAN_INFO_UNTAGGED)
+ vlan->untag |= BIT(port);
+
+ mt7628_vlan_sync(ds);
+ return 0;
+}
+
+static int mt7628_dsa_8021q_vlan_del(struct dsa_switch *ds, int port, u16 vid)
+{
+ struct mt7628_esw *esw = ds->priv;
+ struct mt7628_vlan *vlan = NULL;
+ int i;
+
+ for (i = 0; i < MT7628_NUM_VLANS; i++) {
+ struct mt7628_vlan *check_vlan = &esw->vlans[i];
+
+ if (!check_vlan->active || check_vlan->vid != vid)
+ continue;
+ vlan = check_vlan;
+ break;
+ }
+ if (!vlan)
+ return -ENOENT;
+
+ if (esw->tag_8021q_pvid[port] == vid)
+ esw->tag_8021q_pvid[port] = 0;
+
+ vlan->members &= ~BIT(port);
+ vlan->untag &= ~BIT(port);
+
+ if (!vlan->members) {
+ vlan->active = false;
+ vlan->vid = 0;
+ }
+
+ mt7628_vlan_sync(ds);
+ return 0;
+}
+
+static void mt7628_teardown(struct dsa_switch *ds)
+{
+ rtnl_lock();
+ dsa_tag_8021q_unregister(ds);
+ rtnl_unlock();
+}
+
+static const struct dsa_switch_ops mt7628_switch_ops = {
+ .get_tag_protocol = mt7628_get_tag_proto,
+ .setup = mt7628_setup,
+ .teardown = mt7628_teardown,
+ .port_enable = mt7628_port_enable,
+ .port_disable = mt7628_port_disable,
+ .phylink_get_caps = mt7628_phylink_get_caps,
+ .tag_8021q_vlan_add = mt7628_dsa_8021q_vlan_add,
+ .tag_8021q_vlan_del = mt7628_dsa_8021q_vlan_del,
+};
+
+static int mt7628_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct mt7628_esw *esw;
+ struct dsa_switch *ds;
+ void __iomem *base;
+
+ ds = devm_kzalloc(&pdev->dev, sizeof(*ds), GFP_KERNEL);
+ if (!ds)
+ return -ENOMEM;
+
+ esw = devm_kzalloc(&pdev->dev, sizeof(*esw), GFP_KERNEL);
+ if (!esw)
+ return -ENOMEM;
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ esw->regmap = devm_regmap_init_mmio(&pdev->dev, base,
+ &mt7628_esw_regmap_cfg);
+ if (IS_ERR(esw->regmap))
+ return PTR_ERR(esw->regmap);
+
+ esw->rst_ephy = devm_reset_control_get_exclusive(&pdev->dev, "ephy");
+ if (IS_ERR(esw->rst_ephy))
+ return dev_err_probe(dev, PTR_ERR(esw->rst_ephy),
+ "failed to get EPHY reset\n");
+
+ esw->rst_esw = devm_reset_control_get_exclusive(&pdev->dev, "esw");
+ if (IS_ERR(esw->rst_esw))
+ return dev_err_probe(dev, PTR_ERR(esw->rst_esw),
+ "failed to get ESW reset\n");
+
+ ds->dev = dev;
+ ds->num_ports = MT7628_ESW_NUM_PORTS;
+ ds->ops = &mt7628_switch_ops;
+ ds->priv = esw;
+ esw->ds = ds;
+ esw->dev = dev;
+ dev_set_drvdata(dev, esw);
+
+ return dsa_register_switch(ds);
+}
+
+static void mt7628_remove(struct platform_device *pdev)
+{
+ struct mt7628_esw *esw = platform_get_drvdata(pdev);
+
+ if (!esw)
+ return;
+
+ dsa_unregister_switch(esw->ds);
+}
+
+static void mt7628_shutdown(struct platform_device *pdev)
+{
+ struct mt7628_esw *esw = platform_get_drvdata(pdev);
+
+ if (!esw)
+ return;
+
+ dsa_switch_shutdown(esw->ds);
+ dev_set_drvdata(&pdev->dev, NULL);
+}
+
+static const struct of_device_id mt7628_of_match[] = {
+ { .compatible = "mediatek,mt7628-esw" },
+ {}
+};
+
+MODULE_DEVICE_TABLE(of, mt7628_of_match);
+
+static struct platform_driver mt7628_driver = {
+ .driver = {
+ .name = "mt7628-esw",
+ .of_match_table = mt7628_of_match,
+ },
+ .probe = mt7628_probe,
+ .remove = mt7628_remove,
+ .shutdown = mt7628_shutdown,
+};
+
+module_platform_driver(mt7628_driver);
+
+MODULE_AUTHOR("Joris Vaisvila <joey@tinyisr.com>");
+MODULE_DESCRIPTION("Driver for Mediatek MT7628 embedded switch");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related
* Re: [PATCH net-next v8 3/3] net: airoha: defer GDM3/GDM4 WAN mode and GDM2 loopback to QoS offload
From: Lorenzo Bianconi @ 2026-07-04 11:09 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Alexander Lobakin, linux-arm-kernel, linux-mediatek,
netdev, Madhur Agrawal
In-Reply-To: <20260703-airoha-ethtool-priv_flags-v8-3-015ba5ac89ee@kernel.org>
[-- Attachment #1: Type: text/plain, Size: 22228 bytes --]
> GDM3 and GDM4 ports require GDM2 loopback to be enabled for hardware
> QoS offload to function. Without it, HTB and ETS offload on these ports
> do not work.
> Previously, GDM3/GDM4 ports were automatically configured as WAN with
> GDM2 loopback enabled during ndo_init(). Add the capability to configure
> GDM3/GDM4 as WAN/LAN on demand when QoS offload is created or destroyed.
> Hook airoha_enable_qos_for_gdm34() into TC_HTB_CREATE so that requesting
> HTB offload on a GDM3/GDM4 LAN port switches it to WAN mode and enables
> GDM2 loopback, with proper rollback on failure. Introduce the
> AIROHA_DEV_F_QOS flag to track whether a device has an active HTB
> qdisc; clear it on TC_HTB_DESTROY. The device keeps its WAN role after
> qdisc teardown so that its configuration is preserved until another
> device explicitly needs the WAN role for QoS offload.
> If another GDM3/GDM4 device already holds the WAN role without an active
> QoS qdisc, demote it to LAN before promoting the requesting device. Skip
> the demotion when the requesting device is itself already the WAN device.
> Since airoha_dev_set_qdma() can now be called on a running device to
> migrate between QDMA blocks, make dev->qdma an RCU pointer so the TX
> path can safely dereference it without holding RTNL.
> Hold flow_offload_mutex in airoha_enable_qos_for_gdm34() and
> airoha_disable_qos_for_gdm34() around the dev->flags update,
> airoha_dev_set_qdma() and GDM2 loopback configuration, serializing
> against concurrent airoha_ppe_hw_init() in the TC_SETUP_CLSFLOWER
> offload path.
> Introduce airoha_qdma_deref() helper that wraps rcu_dereference_protected()
> with a lockdep condition accepting either rtnl_lock or flow_offload_mutex,
> and use it across all control-path dereferences of the RCU-protected
> dev->qdma pointer.
> Add airoha_disable_gdm2_loopback() to disable GDM2 hw loopback.
>
> Tested-by: Madhur Agrawal <madhur.agrawal@airoha.com>
> Reviewed-by: Alexander Lobakin <aleksander.lobakin@intel.com>
> Reviewed-by: Simon Horman <horms@kernel.org>
> Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
commenting on sashiko's report:
https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260703-airoha-ethtool-priv_flags-v8-0-015ba5ac89ee%40kernel.org
> ---
> drivers/net/ethernet/airoha/airoha_eth.c | 233 ++++++++++++++++++++++++++----
> drivers/net/ethernet/airoha/airoha_eth.h | 13 +-
> drivers/net/ethernet/airoha/airoha_ppe.c | 9 +-
> drivers/net/ethernet/airoha/airoha_regs.h | 1 +
> 4 files changed, 227 insertions(+), 29 deletions(-)
>
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
> index aaf2a4717d12..fe2e2af67173 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.c
> +++ b/drivers/net/ethernet/airoha/airoha_eth.c
> @@ -934,7 +934,7 @@ static void airoha_qdma_wake_netdev_txqs(struct airoha_queue *q)
> if (!dev)
> continue;
>
> - if (dev->qdma != qdma)
> + if (rcu_access_pointer(dev->qdma) != qdma)
> continue;
>
> netdev = netdev_from_priv(dev);
> @@ -1866,8 +1866,8 @@ static int airoha_dev_open(struct net_device *netdev)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> struct airoha_gdm_port *port = dev->port;
> - struct airoha_qdma *qdma = dev->qdma;
> u32 pse_port = FE_PSE_PORT_PPE1;
> + struct airoha_qdma *qdma;
> int err;
>
> netif_tx_start_all_queues(netdev);
> @@ -1875,6 +1875,7 @@ static int airoha_dev_open(struct net_device *netdev)
> if (err)
> return err;
>
> + qdma = airoha_qdma_deref(dev);
> if (netdev_uses_dsa(netdev))
> airoha_fe_set(qdma->eth, REG_GDM_INGRESS_CFG(port->id),
> GDM_STAG_EN_MASK);
> @@ -1898,7 +1899,6 @@ static int airoha_dev_stop(struct net_device *netdev)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> struct airoha_gdm_port *port = dev->port;
> - struct airoha_qdma *qdma = dev->qdma;
>
> netif_tx_disable(netdev);
> airoha_set_vip_for_gdm_port(dev, false);
> @@ -1906,7 +1906,7 @@ static int airoha_dev_stop(struct net_device *netdev)
> if (--port->users)
> airoha_ppe_set_xmit_frame_size(dev);
> else
> - airoha_set_gdm_port_fwd_cfg(qdma->eth,
> + airoha_set_gdm_port_fwd_cfg(dev->eth,
> REG_GDM_FWD_CFG(port->id),
> FE_PSE_PORT_DROP);
> return 0;
> @@ -1989,6 +1989,53 @@ static int airoha_enable_gdm2_loopback(struct airoha_gdm_dev *dev)
> return 0;
> }
>
> +static int airoha_disable_gdm2_loopback(struct airoha_gdm_dev *dev)
> +{
> + struct airoha_gdm_port *port = dev->port;
> + struct airoha_eth *eth = dev->eth;
> + int i, src_port;
> + u32 pse_port;
> +
> + src_port = eth->soc->ops.get_sport(dev->port, dev->nbq);
> + if (src_port < 0)
> + return src_port;
> +
> + airoha_fe_clear(eth,
> + REG_SP_DFT_CPORT(src_port >> fls(SP_CPORT_DFT_MASK)),
> + SP_CPORT_MASK(src_port & SP_CPORT_DFT_MASK));
> +
> + airoha_fe_set(eth, REG_GDM_FWD_CFG(AIROHA_GDM2_IDX),
> + GDM_STRIP_CRC_MASK);
> + airoha_set_gdm_port_fwd_cfg(eth, REG_GDM_FWD_CFG(AIROHA_GDM2_IDX),
> + FE_PSE_PORT_DROP);
> + airoha_fe_clear(eth, REG_GDM_LPBK_CFG(AIROHA_GDM2_IDX),
> + LPBK_CHAN_MASK | LPBK_MODE_MASK | LPBK_EN_MASK);
> + pse_port = airoha_ppe_is_enabled(eth, 1) ? FE_PSE_PORT_PPE2
> + : FE_PSE_PORT_PPE1;
> + airoha_set_gdm_port_fwd_cfg(eth, REG_GDM_FWD_CFG(AIROHA_GDM2_IDX),
> + pse_port);
> +
> + airoha_fe_rmw(eth, REG_FE_WAN_PORT, WAN0_MASK,
> + FIELD_PREP(WAN0_MASK, AIROHA_GDM2_IDX));
> +
> + for (i = 0; i < eth->soc->num_ppe; i++)
> + airoha_fe_clear(eth, REG_PPE_DFT_CPORT(i, AIROHA_GDM2_IDX),
> + DFT_CPORT_MASK(AIROHA_GDM2_IDX));
> +
> + /* Enable VIP and IFC for GDM2 */
> + airoha_fe_set(eth, REG_FE_VIP_PORT_EN, BIT(AIROHA_GDM2_IDX));
> + airoha_fe_set(eth, REG_FE_IFC_PORT_EN, BIT(AIROHA_GDM2_IDX));
> +
> + if (port->id == AIROHA_GDM4_IDX && airoha_is_7581(eth)) {
> + u32 mask = FC_ID_OF_SRC_PORT_MASK(dev->nbq);
> +
> + airoha_fe_rmw(eth, REG_SRC_PORT_FC_MAP6, mask,
> + FC_MAP6_DEF_VALUE & mask);
> + }
> +
> + return 0;
> +}
> +
> static struct airoha_gdm_dev *
> airoha_get_wan_gdm_dev(struct airoha_eth *eth)
> {
> @@ -2015,15 +2062,35 @@ airoha_get_wan_gdm_dev(struct airoha_eth *eth)
> static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)
> {
> struct net_device *netdev = netdev_from_priv(dev);
> + struct airoha_qdma *cur_qdma, *qdma;
> struct airoha_eth *eth = dev->eth;
> - int ppe_id;
> + int i, ppe_id;
>
> /* QDMA0 is used for lan ports while QDMA1 is used for WAN ports */
> - dev->qdma = ð->qdma[!airoha_is_lan_gdm_dev(dev)];
> - netdev->irq = dev->qdma->irq_banks[0].irq;
> + qdma = ð->qdma[!airoha_is_lan_gdm_dev(dev)];
> + cur_qdma = airoha_qdma_deref(dev);
> +
> + rcu_assign_pointer(dev->qdma, qdma);
> + netdev->irq = qdma->irq_banks[0].irq;
>
> ppe_id = !airoha_is_lan_gdm_dev(dev) && airoha_ppe_is_enabled(eth, 1);
> airoha_ppe_set_cpu_port(dev, ppe_id, airoha_get_fe_port(dev));
> +
> + if (!cur_qdma)
> + return;
> +
> + /* Seed qos_stats baselines with the new QDMA block's current
> + * counter values to avoid a spurious spike on the first stats
> + * query after migration.
> + */
> + for (i = 0; i < ARRAY_SIZE(dev->qos_stats); i++) {
> + dev->qos_stats[i].cpu_tx_packets =
> + airoha_qdma_rr(qdma, REG_CNTR_VAL(i << 1));
> + dev->qos_stats[i].fwd_tx_packets =
> + airoha_qdma_rr(qdma, REG_CNTR_VAL((i << 1) + 1));
> + }
> + synchronize_rcu();
> + netif_tx_wake_all_queues(netdev);
> }
- Is the ordering here safe with respect to the TX fast path?
- There is a small window where in-flight packets can see the hw not fully
re-configured (e.g. FE not fully re-configured) but this is not harmful
(e.g. causing persistent state corruption or hardware), just some packets
are drop during user-requested configuration. I think we can ignore this
comment.
>
> static int airoha_dev_init(struct net_device *netdev)
> @@ -2178,9 +2245,9 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
> struct net_device *netdev)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> - struct airoha_qdma *qdma = dev->qdma;
> u32 nr_frags, tag, msg0, msg1, len;
> struct airoha_queue_entry *e;
> + struct airoha_qdma *qdma;
> struct netdev_queue *txq;
> struct airoha_queue *q;
> LIST_HEAD(tx_list);
> @@ -2189,6 +2256,8 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
> u16 index;
> u8 fport;
>
> + rcu_read_lock();
> + qdma = rcu_dereference(dev->qdma);
> qid = airoha_qdma_get_txq(qdma, skb_get_queue_mapping(skb));
> tag = airoha_get_dsa_tag(skb, netdev);
>
> @@ -2238,6 +2307,8 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
> netif_tx_stop_queue(txq);
> q->txq_stopped = true;
> spin_unlock_bh(&q->lock);
> + rcu_read_unlock();
> +
> return NETDEV_TX_BUSY;
> }
>
> @@ -2303,6 +2374,7 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
> FIELD_PREP(TX_RING_CPU_IDX_MASK, index));
>
> spin_unlock_bh(&q->lock);
> + rcu_read_unlock();
>
> return NETDEV_TX_OK;
>
> @@ -2315,6 +2387,7 @@ static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
> error:
> dev_kfree_skb_any(skb);
> netdev->stats.tx_dropped++;
> + rcu_read_unlock();
>
> return NETDEV_TX_OK;
> }
> @@ -2394,17 +2467,19 @@ static int airoha_qdma_set_chan_tx_sched(struct net_device *netdev,
> const u16 *weights, u8 n_weights)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> + struct airoha_qdma *qdma;
> int i;
>
> + qdma = airoha_qdma_deref(dev);
> for (i = 0; i < AIROHA_NUM_QOS_QUEUES; i++)
> - airoha_qdma_clear(dev->qdma, REG_QUEUE_CLOSE_CFG(channel),
> + airoha_qdma_clear(qdma, REG_QUEUE_CLOSE_CFG(channel),
> TXQ_DISABLE_CHAN_QUEUE_MASK(channel, i));
>
> for (i = 0; i < n_weights; i++) {
> u32 status;
> int err;
>
> - airoha_qdma_wr(dev->qdma, REG_TXWRR_WEIGHT_CFG,
> + airoha_qdma_wr(qdma, REG_TXWRR_WEIGHT_CFG,
> TWRR_RW_CMD_MASK |
> FIELD_PREP(TWRR_CHAN_IDX_MASK, channel) |
> FIELD_PREP(TWRR_QUEUE_IDX_MASK, i) |
> @@ -2412,12 +2487,12 @@ static int airoha_qdma_set_chan_tx_sched(struct net_device *netdev,
> err = read_poll_timeout(airoha_qdma_rr, status,
> status & TWRR_RW_CMD_DONE,
> USEC_PER_MSEC, 10 * USEC_PER_MSEC,
> - true, dev->qdma, REG_TXWRR_WEIGHT_CFG);
> + true, qdma, REG_TXWRR_WEIGHT_CFG);
> if (err)
> return err;
> }
>
> - airoha_qdma_rmw(dev->qdma, REG_CHAN_QOS_MODE(channel >> 3),
> + airoha_qdma_rmw(qdma, REG_CHAN_QOS_MODE(channel >> 3),
> CHAN_QOS_MODE_MASK(channel),
> __field_prep(CHAN_QOS_MODE_MASK(channel), mode));
>
> @@ -2481,10 +2556,11 @@ static int airoha_qdma_get_tx_ets_stats(struct net_device *netdev, int channel,
> struct tc_ets_qopt_offload *opt)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> - struct airoha_qdma *qdma = dev->qdma;
> u32 cpu_tx_packets, fwd_tx_packets;
> + struct airoha_qdma *qdma;
> u64 tx_packets;
>
> + qdma = airoha_qdma_deref(dev);
> cpu_tx_packets = airoha_qdma_rr(qdma, REG_CNTR_VAL(channel << 1));
> fwd_tx_packets = airoha_qdma_rr(qdma,
> REG_CNTR_VAL((channel << 1) + 1));
> @@ -2751,16 +2827,18 @@ static int airoha_qdma_set_tx_rate_limit(struct net_device *netdev,
> u32 bucket_size)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> + struct airoha_qdma *qdma;
> int i, err;
>
> + qdma = airoha_qdma_deref(dev);
> for (i = 0; i <= TRTCM_PEAK_MODE; i++) {
> - err = airoha_qdma_set_trtcm_config(dev->qdma, channel,
> + err = airoha_qdma_set_trtcm_config(qdma, channel,
> REG_EGRESS_TRTCM_CFG, i,
> !!rate, TRTCM_METER_MODE);
> if (err)
> return err;
>
> - err = airoha_qdma_set_trtcm_token_bucket(dev->qdma, channel,
> + err = airoha_qdma_set_trtcm_token_bucket(qdma, channel,
> REG_EGRESS_TRTCM_CFG,
> i, rate, bucket_size);
> if (err)
> @@ -2796,11 +2874,12 @@ static int airoha_tc_htb_alloc_leaf_queue(struct net_device *netdev,
> u32 channel = TC_H_MIN(opt->classid) % AIROHA_NUM_QOS_CHANNELS;
> int err, num_tx_queues = AIROHA_NUM_TX_RING + channel + 1;
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> - struct airoha_qdma *qdma = dev->qdma;
> + struct airoha_qdma *qdma;
>
> /* Here we need to check the requested QDMA channel is not already
> * in use by another net_device running on the same QDMA block.
> */
> + qdma = airoha_qdma_deref(dev);
> if (test_and_set_bit(channel, qdma->qos_channel_map)) {
> NL_SET_ERR_MSG_MOD(opt->extack,
> "qdma qos channel already in use");
> @@ -2836,7 +2915,7 @@ static int airoha_qdma_set_rx_meter(struct airoha_gdm_dev *dev,
> u32 rate, u32 bucket_size,
> enum trtcm_unit_type unit_type)
> {
> - struct airoha_qdma *qdma = dev->qdma;
> + struct airoha_qdma *qdma = airoha_qdma_deref(dev);
> int i;
>
> for (i = 0; i < ARRAY_SIZE(qdma->q_rx); i++) {
> @@ -3011,10 +3090,11 @@ static void airoha_tc_remove_htb_queue(struct net_device *netdev, int queue)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> int num_tx_queues = AIROHA_NUM_TX_RING;
> - struct airoha_qdma *qdma = dev->qdma;
> + struct airoha_qdma *qdma;
>
> airoha_qdma_set_tx_rate_limit(netdev, queue, 0, 0);
>
> + qdma = airoha_qdma_deref(dev);
> clear_bit(queue, qdma->qos_channel_map);
> clear_bit(queue, dev->qos_sq_bmap);
>
> @@ -3040,6 +3120,98 @@ static int airoha_tc_htb_delete_leaf_queue(struct net_device *netdev,
> return 0;
> }
>
> +static void airoha_disable_qos_for_gdm34(struct net_device *netdev)
> +{
> + struct airoha_gdm_dev *dev = netdev_priv(netdev);
> + struct airoha_gdm_port *port = dev->port;
> + int err;
> +
> + if (port->id != AIROHA_GDM3_IDX &&
> + port->id != AIROHA_GDM4_IDX)
> + return;
> +
> + err = airoha_disable_gdm2_loopback(dev);
> + if (err)
> + netdev_warn(netdev,
> + "failed disabling GDM2 loopback: %d\n", err);
> +
> + dev->flags &= ~AIROHA_DEV_F_WAN;
> + airoha_dev_set_qdma(dev);
> +
> + airoha_set_macaddr(dev, netdev->dev_addr);
- Should the return value of airoha_set_macaddr() be checked here?
- This issue has been reported and discussed in previous iterations.
airoha_set_macaddr() can fail just if the device is misconfigured.
Moreover, airoha_set_macaddr() already logs warnings via dev_warn()
when the MSB check fails, so the failure won't be silent.
> + airoha_ppe_set_xmit_frame_size(dev);
> +
> + if (netif_running(netdev))
> + airoha_set_gdm_port_fwd_cfg(dev->eth,
> + REG_GDM_FWD_CFG(port->id),
> + FE_PSE_PORT_PPE1);
> +}
> +
> +static int airoha_enable_qos_for_gdm34(struct net_device *netdev,
> + struct netlink_ext_ack *extack)
> +{
> + struct airoha_gdm_dev *wan_dev, *dev = netdev_priv(netdev);
> + struct airoha_gdm_port *port = dev->port;
> + struct airoha_eth *eth = dev->eth;
> + int err = -EBUSY;
> +
> + if (port->id != AIROHA_GDM3_IDX &&
> + port->id != AIROHA_GDM4_IDX) {
> + /* HW QoS is always supported by GDM1 and GDM2 */
> + return 0;
> + }
> +
> + if (!airoha_is_lan_gdm_dev(dev)) /* Already enabled */
> + return 0;
> +
> + mutex_lock(&flow_offload_mutex);
> +
> + wan_dev = airoha_get_wan_gdm_dev(eth);
> + if (wan_dev) {
> + if ((wan_dev->flags & AIROHA_DEV_F_QOS) ||
> + wan_dev->port->id == AIROHA_GDM2_IDX) {
> + NL_SET_ERR_MSG_MOD(extack,
> + "QoS configured for WAN device");
> + goto error_unlock;
> + }
> + airoha_disable_qos_for_gdm34(netdev_from_priv(wan_dev));
> + }
> +
> + dev->flags |= AIROHA_DEV_F_WAN;
> + airoha_dev_set_qdma(dev);
> + err = airoha_enable_gdm2_loopback(dev);
> + if (err)
> + goto error_disable_wan;
> +
> + err = airoha_set_macaddr(dev, netdev->dev_addr);
> + if (err)
> + goto error_disable_loopback;
> +
> + airoha_dev_set_xmit_frame_size(netdev);
> + if (netif_running(netdev)) {
> + u32 pse_port;
> +
> + pse_port = airoha_ppe_is_enabled(eth, 1) ? FE_PSE_PORT_PPE2
> + : FE_PSE_PORT_PPE1;
> + airoha_set_gdm_port_fwd_cfg(eth, REG_GDM_FWD_CFG(port->id),
> + pse_port);
> + }
> +
> + mutex_unlock(&flow_offload_mutex);
> +
> + return 0;
> +
> +error_disable_loopback:
> + airoha_disable_gdm2_loopback(dev);
> +error_disable_wan:
> + dev->flags &= ~AIROHA_DEV_F_WAN;
> + airoha_dev_set_qdma(dev);
> +error_unlock:
> + mutex_unlock(&flow_offload_mutex);
> +
> + return err;
- Does this rollback path fully undo the effect of demoting the previous WAN device?
- This issue has been reported and discussed in previous iterations.
If the promotion of the new device fails, the tc command returns an error to userspace,
the requested configuration was not applied. The system is still processing
packets and the user can log and reapply the configuration.
Re-promoting the old WAN device on error would add complexity for failure cases that
do not really occur in practice since airoha_set_macaddr() and
airoha_enable_gdm2_loopback() fail only if the device is misconfigured.
Regards,
Lorenzo
> +}
> +
> static int airoha_tc_htb_destroy(struct net_device *netdev)
> {
> struct airoha_gdm_dev *dev = netdev_priv(netdev);
> @@ -3048,6 +3220,8 @@ static int airoha_tc_htb_destroy(struct net_device *netdev)
> for_each_set_bit(q, dev->qos_sq_bmap, AIROHA_NUM_QOS_CHANNELS)
> airoha_tc_remove_htb_queue(netdev, q);
>
> + dev->flags &= ~AIROHA_DEV_F_QOS;
> +
> return 0;
> }
>
> @@ -3067,24 +3241,33 @@ static int airoha_tc_get_htb_get_leaf_queue(struct net_device *netdev,
> return 0;
> }
>
> -static int airoha_tc_setup_qdisc_htb(struct net_device *dev,
> +static int airoha_tc_setup_qdisc_htb(struct net_device *netdev,
> struct tc_htb_qopt_offload *opt)
> {
> switch (opt->command) {
> - case TC_HTB_CREATE:
> + case TC_HTB_CREATE: {
> + struct airoha_gdm_dev *dev = netdev_priv(netdev);
> + int err;
> +
> + err = airoha_enable_qos_for_gdm34(netdev, opt->extack);
> + if (err)
> + return err;
> +
> + dev->flags |= AIROHA_DEV_F_QOS;
> break;
> + }
> case TC_HTB_DESTROY:
> - return airoha_tc_htb_destroy(dev);
> + return airoha_tc_htb_destroy(netdev);
> case TC_HTB_NODE_MODIFY:
> - return airoha_tc_htb_modify_queue(dev, opt);
> + return airoha_tc_htb_modify_queue(netdev, opt);
> case TC_HTB_LEAF_ALLOC_QUEUE:
> - return airoha_tc_htb_alloc_leaf_queue(dev, opt);
> + return airoha_tc_htb_alloc_leaf_queue(netdev, opt);
> case TC_HTB_LEAF_DEL:
> case TC_HTB_LEAF_DEL_LAST:
> case TC_HTB_LEAF_DEL_LAST_FORCE:
> - return airoha_tc_htb_delete_leaf_queue(dev, opt);
> + return airoha_tc_htb_delete_leaf_queue(netdev, opt);
> case TC_HTB_LEAF_QUERY_QUEUE:
> - return airoha_tc_get_htb_get_leaf_queue(dev, opt);
> + return airoha_tc_get_htb_get_leaf_queue(netdev, opt);
> default:
> return -EOPNOTSUPP;
> }
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
> index bf44be9f0954..4d03ebae1a2d 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.h
> +++ b/drivers/net/ethernet/airoha/airoha_eth.h
> @@ -545,11 +545,12 @@ struct airoha_qdma {
>
> enum airoha_dev_flags {
> AIROHA_DEV_F_WAN = BIT(0),
> + AIROHA_DEV_F_QOS = BIT(1),
> };
>
> struct airoha_gdm_dev {
> + struct airoha_qdma __rcu *qdma;
> struct airoha_gdm_port *port;
> - struct airoha_qdma *qdma;
> struct airoha_eth *eth;
>
> DECLARE_BITMAP(qos_sq_bmap, AIROHA_NUM_QOS_CHANNELS);
> @@ -685,6 +686,16 @@ int airoha_get_fe_port(struct airoha_gdm_dev *dev);
> bool airoha_is_valid_gdm_dev(struct airoha_eth *eth,
> struct airoha_gdm_dev *dev);
>
> +extern struct mutex flow_offload_mutex;
> +
> +static inline struct airoha_qdma *
> +airoha_qdma_deref(struct airoha_gdm_dev *dev)
> +{
> + return rcu_dereference_protected(dev->qdma,
> + lockdep_rtnl_is_held() ||
> + lockdep_is_held(&flow_offload_mutex));
> +}
> +
> void airoha_ppe_set_xmit_frame_size(struct airoha_gdm_dev *dev);
> void airoha_ppe_set_cpu_port(struct airoha_gdm_dev *dev, u8 ppe_id, u8 fport);
> bool airoha_ppe_is_enabled(struct airoha_eth *eth, int index);
> diff --git a/drivers/net/ethernet/airoha/airoha_ppe.c b/drivers/net/ethernet/airoha/airoha_ppe.c
> index e7c78293002a..78378a6262f9 100644
> --- a/drivers/net/ethernet/airoha/airoha_ppe.c
> +++ b/drivers/net/ethernet/airoha/airoha_ppe.c
> @@ -15,7 +15,10 @@
> #include "airoha_regs.h"
> #include "airoha_eth.h"
>
> -static DEFINE_MUTEX(flow_offload_mutex);
> +/* Serialize airoha_gdm_dev flags, QDMA pointer and PPE CPU port
> + * configuration.
> + */
> +DEFINE_MUTEX(flow_offload_mutex);
> static DEFINE_SPINLOCK(ppe_lock);
>
> static const struct rhashtable_params airoha_flow_table_params = {
> @@ -86,8 +89,8 @@ static u32 airoha_ppe_get_timestamp(struct airoha_ppe *ppe)
>
> void airoha_ppe_set_cpu_port(struct airoha_gdm_dev *dev, u8 ppe_id, u8 fport)
> {
> - struct airoha_qdma *qdma = dev->qdma;
> - struct airoha_eth *eth = qdma->eth;
> + struct airoha_qdma *qdma = airoha_qdma_deref(dev);
> + struct airoha_eth *eth = dev->eth;
> u8 qdma_id = qdma - ð->qdma[0];
> u32 fe_cpu_port;
>
> diff --git a/drivers/net/ethernet/airoha/airoha_regs.h b/drivers/net/ethernet/airoha/airoha_regs.h
> index 6fed63d013b4..442b48c9b991 100644
> --- a/drivers/net/ethernet/airoha/airoha_regs.h
> +++ b/drivers/net/ethernet/airoha/airoha_regs.h
> @@ -375,6 +375,7 @@
>
> #define REG_SRC_PORT_FC_MAP6 0x2298
> #define FC_ID_OF_SRC_PORT_MASK(_n) GENMASK(4 + ((_n) << 3), ((_n) << 3))
> +#define FC_MAP6_DEF_VALUE 0x1b1a1918
>
> #define REG_WAN_MTU0 0x2300
> #define WAN_MTU1_MASK GENMASK(29, 16)
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* [PATCH net] tun/tap & vhost-net: make qdisc backpressure opt-in via IFF_BACKPRESSURE
From: Simon Schippers @ 2026-07-04 11:20 UTC (permalink / raw)
To: Willem de Bruijn, Jason Wang, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Michael S . Tsirkin, netdev
Cc: Simon Horman, Jonathan Corbet, Shuah Khan, Andrew Lunn,
Tim Gebauer, Brett Sheffield, linux-doc, linux-kernel,
Simon Schippers
Commit 1d6e569b7d0c ("tun/tap & vhost-net: avoid ptr_ring tail-drop
when a qdisc is present") did not show a relevant performance regression
in my testing but on Brett Sheffield's librecast testbed it shows a
significant performance drop. The regression can be pinpointed when
multiple iperf3 TCP threads are sending. For 8 threads the performance
dropped from 13.5 Gbit/s to 9.13 Gbit/s. This is the reason why this
patch makes the qdisc backpressure behavior opt-in.
One option to accomplish the opt-in would be to set the default qdisc to
noqueue at init. However this may also break userspace as users might
have chosen a custom qdisc even though most of the qdiscs did nothing
for tun/tap in the past due to missing backpressure...
This is the reason why in this patch, the flag IFF_BACKPRESSURE is
introduced instead which is required to enable the backpressure logic.
This means the stopping logic in tun_net_xmit() and the waking logic in
__tun_wake_queue() are skipped if the flag is disabled.
In tun_set_iff(), netif_tx_wake_all_queues() is replaced with looping
over all tfiles in which the netdev queues are woken and cons_cnt is
reset while the consumer_lock and producer_lock are held. This is to
ensure that tun_net_xmit() can not stop the queue concurrently, avoiding
a possible stall.
The documentation in tuntap.rst is updated accordingly.
Fixes: 1d6e569b7d0c ("tun/tap & vhost-net: avoid ptr_ring tail-drop when a qdisc is present")
Reported-by: Brett Sheffield <brett@librecast.net>
Closes: https://lore.kernel.org/netdev/akVnoOYQOrt8k-Gu@karahi.librecast.net/T/#u
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
Documentation/networking/tuntap.rst | 17 +++++++++++++++++
drivers/net/tun.c | 29 +++++++++++++++++++++++------
include/uapi/linux/if_tun.h | 1 +
tools/include/uapi/linux/if_tun.h | 1 +
4 files changed, 42 insertions(+), 6 deletions(-)
diff --git a/Documentation/networking/tuntap.rst b/Documentation/networking/tuntap.rst
index 4d7087f727be..599264825dd2 100644
--- a/Documentation/networking/tuntap.rst
+++ b/Documentation/networking/tuntap.rst
@@ -206,6 +206,23 @@ enable is true we enable it, otherwise we disable it::
return ioctl(fd, TUNSETQUEUE, (void *)&ifr);
}
+3.4 qdisc backpressure
+----------------------
+
+Starting with Linux 7.2, IFF_BACKPRESSURE can be set to enable qdisc
+backpressure. Without it, TX drops occur when the internal ring buffer is
+full. With it, the kernel stops the TX queue instead, letting the qdisc
+hold packets. Drops only occur as a rare race. This can benefit protocols
+like TCP that react to drops. Backpressure requires a qdisc to be
+attached and has no effect with noqueue.
+
+The TUN/TAP ring buffer size can be reduced alongside this flag to
+further shift buffering into the qdisc and reduce bufferbloat, but comes
+at possible performance cost.
+
+When running multiple network streams in parallel, the flag may reduce
+performance due to the extra overhead of the backpressure mechanism.
+
Universal TUN/TAP device driver Frequently Asked Question
=========================================================
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index ffbe6f13fb1f..3bf8a73a0816 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -98,7 +98,8 @@ static void tun_default_link_ksettings(struct net_device *dev,
#define TUN_FASYNC IFF_ATTACH_QUEUE
#define TUN_FEATURES (IFF_NO_PI | IFF_ONE_QUEUE | IFF_VNET_HDR | \
- IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS)
+ IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS | \
+ IFF_BACKPRESSURE)
#define GOODCOPY_LEN 128
@@ -1077,7 +1078,8 @@ static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
spin_lock(&tfile->tx_ring.producer_lock);
ret = __ptr_ring_produce(&tfile->tx_ring, skb);
- if (!qdisc_txq_has_no_queue(queue) &&
+ if ((tun->flags & IFF_BACKPRESSURE) &&
+ !qdisc_txq_has_no_queue(queue) &&
__ptr_ring_check_produce(&tfile->tx_ring) == -ENOSPC) {
netif_tx_stop_queue(queue);
/* Paired with smp_mb() in __tun_wake_queue() */
@@ -2151,8 +2153,12 @@ static ssize_t tun_put_user(struct tun_struct *tun,
static void __tun_wake_queue(struct tun_struct *tun,
struct tun_file *tfile, int consumed)
{
- struct netdev_queue *txq = netdev_get_tx_queue(tun->dev,
- tfile->queue_index);
+ struct netdev_queue *txq;
+
+ if (!(tun->flags & IFF_BACKPRESSURE))
+ return;
+
+ txq = netdev_get_tx_queue(tun->dev, tfile->queue_index);
/* Paired with smp_mb__after_atomic() in tun_net_xmit() */
smp_mb();
@@ -2893,8 +2899,19 @@ static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
/* Make sure persistent devices do not get stuck in
* xoff state.
*/
- if (netif_running(tun->dev))
- netif_tx_wake_all_queues(tun->dev);
+ if (netif_running(tun->dev)) {
+ for (int i = 0; i < tun->numqueues; i++) {
+ struct tun_file *i_tfile;
+
+ i_tfile = rtnl_dereference(tun->tfiles[i]);
+ spin_lock_bh(&i_tfile->tx_ring.consumer_lock);
+ spin_lock(&i_tfile->tx_ring.producer_lock);
+ netif_wake_subqueue(tun->dev, i_tfile->queue_index);
+ i_tfile->cons_cnt = 0;
+ spin_unlock(&i_tfile->tx_ring.producer_lock);
+ spin_unlock_bh(&i_tfile->tx_ring.consumer_lock);
+ }
+ }
strscpy(ifr->ifr_name, tun->dev->name);
return 0;
diff --git a/include/uapi/linux/if_tun.h b/include/uapi/linux/if_tun.h
index 79d53c7a1ebd..73a77141315c 100644
--- a/include/uapi/linux/if_tun.h
+++ b/include/uapi/linux/if_tun.h
@@ -69,6 +69,7 @@
#define IFF_NAPI_FRAGS 0x0020
/* Used in TUNSETIFF to bring up tun/tap without carrier */
#define IFF_NO_CARRIER 0x0040
+#define IFF_BACKPRESSURE 0x0080
#define IFF_NO_PI 0x1000
/* This flag has no real effect */
#define IFF_ONE_QUEUE 0x2000
diff --git a/tools/include/uapi/linux/if_tun.h b/tools/include/uapi/linux/if_tun.h
index 2ec07de1d73b..97b670f5bc0a 100644
--- a/tools/include/uapi/linux/if_tun.h
+++ b/tools/include/uapi/linux/if_tun.h
@@ -67,6 +67,7 @@
#define IFF_TAP 0x0002
#define IFF_NAPI 0x0010
#define IFF_NAPI_FRAGS 0x0020
+#define IFF_BACKPRESSURE 0x0080
#define IFF_NO_PI 0x1000
/* This flag has no real effect */
#define IFF_ONE_QUEUE 0x2000
--
2.43.0
^ permalink raw reply related
* Re: [PATCH net] tun/tap & vhost-net: make qdisc backpressure opt-in via IFF_BACKPRESSURE
From: Brett Sheffield @ 2026-07-04 11:58 UTC (permalink / raw)
To: Simon Schippers
Cc: Willem de Bruijn, Jason Wang, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Michael S . Tsirkin, netdev,
Simon Horman, Jonathan Corbet, Shuah Khan, Andrew Lunn,
Tim Gebauer, linux-doc, linux-kernel
In-Reply-To: <20260704112058.95421-1-simon.schippers@tu-dortmund.de>
On 2026-07-04 13:20, Simon Schippers wrote:
> Commit 1d6e569b7d0c ("tun/tap & vhost-net: avoid ptr_ring tail-drop
> when a qdisc is present") did not show a relevant performance regression
> in my testing but on Brett Sheffield's librecast testbed it shows a
> significant performance drop. The regression can be pinpointed when
> multiple iperf3 TCP threads are sending. For 8 threads the performance
> dropped from 13.5 Gbit/s to 9.13 Gbit/s. This is the reason why this
> patch makes the qdisc backpressure behavior opt-in.
Thanks Simon.
I've tested and confirmed this fixes the original failing IPv6 multicast (UDP)
test which flagged the problem.
Tested-by: Brett A C Sheffield <bacs@librecast.net>
Cheers,
Brett
--
Brett Sheffield (he/him)
Librecast - Decentralising the Internet with Multicast
https://librecast.net/
https://blog.brettsheffield.com/
^ permalink raw reply
* [PATCH net-next] net: chelsio: cxgb4: Use str_plural() in mem_intr_handler()
From: Thorsten Blum @ 2026-07-04 12:10 UTC (permalink / raw)
To: Potnuri Bharat Teja, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: Thorsten Blum, netdev, linux-kernel
Replace the manual ternary "s" pluralization with str_plural() to
simplify the code.
Signed-off-by: Thorsten Blum <thorsten.blum@linux.dev>
---
drivers/net/ethernet/chelsio/cxgb4/t4_hw.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/net/ethernet/chelsio/cxgb4/t4_hw.c b/drivers/net/ethernet/chelsio/cxgb4/t4_hw.c
index 6871127427fa..29d7b786e51e 100644
--- a/drivers/net/ethernet/chelsio/cxgb4/t4_hw.c
+++ b/drivers/net/ethernet/chelsio/cxgb4/t4_hw.c
@@ -33,6 +33,7 @@
*/
#include <linux/delay.h>
+#include <linux/string_choices.h>
#include "cxgb4.h"
#include "t4_regs.h"
#include "t4_values.h"
@@ -4867,7 +4868,7 @@ static void mem_intr_handler(struct adapter *adapter, int idx)
if (printk_ratelimit())
dev_warn(adapter->pdev_dev,
"%u %s correctable ECC data error%s\n",
- cnt, name[idx], cnt > 1 ? "s" : "");
+ cnt, name[idx], str_plural(cnt));
}
if (v & ECC_UE_INT_CAUSE_F)
dev_alert(adapter->pdev_dev,
^ permalink raw reply related
* [PATCH] net: xscale: add missing MODULE_DEVICE_TABLE()
From: Pengpeng Hou @ 2026-07-04 12:27 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Richard Cochran
Cc: Pengpeng Hou, netdev, linux-kernel
The driver has an OF match table wired to .of_match_table, but does
not export the table with MODULE_DEVICE_TABLE().
Add the missing MODULE_DEVICE_TABLE(of, ...) entry so module alias
information is generated for OF based module autoloading.
This is a source-level fix. It does not claim dynamic hardware
reproduction; the evidence is the driver-owned match table, its use by
the platform driver, and the missing module alias publication.
Signed-off-by: Pengpeng Hou <pengpeng@iscas.ac.cn>
---
drivers/net/ethernet/xscale/ptp_ixp46x.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/net/ethernet/xscale/ptp_ixp46x.c b/drivers/net/ethernet/xscale/ptp_ixp46x.c
index 558c4f8d23f7..c805d1df5d4e 100644
--- a/drivers/net/ethernet/xscale/ptp_ixp46x.c
+++ b/drivers/net/ethernet/xscale/ptp_ixp46x.c
@@ -307,6 +307,7 @@ static const struct of_device_id ptp_ixp_match[] = {
},
{ },
};
+MODULE_DEVICE_TABLE(of, ptp_ixp_match);
static struct platform_driver ptp_ixp_driver = {
.driver = {
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox