* Re: VXLAN FDB nexthop groups: per-nexthop UDP dst port / VNI feasibility
From: Jack Ma @ 2026-07-12 20:46 UTC (permalink / raw)
To: Ido Schimmel
Cc: David Ahern, Roopa Prabhu, netdev@vger.kernel.org,
Nikolay Aleksandrov
In-Reply-To: <20260712070543.GA126372@shredder>
Thank you, Ido, for the comments. I'm new to this side of the work, so please excuse my plain-text email handling!
I have submitted the patch here: https://lore.kernel.org/netdev/20260712191218.236-1-jack4it@gmail.com/. I also switched to my Gmail address for easier git and email workflow. Thanks to David as well!
To give some context on my approach: I am implementing a multi-tenant VXLAN overlay where we pack multiple VTEPs onto a shared pool of machines. Because we can't assign a directly routable IP to each individual VTEP, I am trying to route traffic from the shared host IP to a specific VTEP using unique port mappings. This is running in a Kubernetes/pod-based environment, if that helps clarify the use case.
Thank you so much, and I hope the patch can be reviewed.
Best,
-Jack
________________________________________
From: Ido Schimmel <idosch@nvidia.com>
Sent: Sunday, July 12, 2026 12:05 AM
To: Jack Ma <jack4it@hotmail.com>
Cc: David Ahern <dsahern@kernel.org>; Roopa Prabhu <roopa@nvidia.com>; netdev@vger.kernel.org <netdev@vger.kernel.org>; Nikolay Aleksandrov <razor@blackwall.org>
Subject: Re: VXLAN FDB nexthop groups: per-nexthop UDP dst port / VNI feasibility
On Fri, Jul 10, 2026 at 05:30:40AM +0000, Jack Ma wrote:
> Thanks very much for any pointers.
I already commented here:
https://lore.kernel.org/netdev/20260709102247.GA1536940@shredder/
^ permalink raw reply
* Re: (subset) [PATCH v2 0/5] ARM: rockchip: rv1126: Add support for Alientek ATK-DLRV1126
From: Heiko Stuebner @ 2026-07-12 20:33 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
David Wu, Maxime Coquelin, Alexandre Torgue, Frank, Andrew Lunn,
Heiner Kallweit, Russell King, Yanan He
Cc: Heiko Stuebner, devicetree, linux-kernel, linux-arm-kernel,
linux-rockchip, netdev, linux-stm32
In-Reply-To: <20260706-rv1126-alientek-dlrv1126-v2-0-ff3176ca362b@gmail.com>
On Mon, 06 Jul 2026 17:14:40 +0800, Yanan He wrote:
> The ATK-DLRV1126 board consists of a CLRV1126F core module and a
> DLRV1126 carrier board. The core module contains the Rockchip RV1126
> SoC, eMMC and RK809 PMIC. The carrier board provides Gigabit Ethernet,
> SD card, AP6212 WiFi and Bluetooth, PCF8563 RTC, ADC keys, GPIO LEDs and
> audio connectors.
>
> This series adds the Alientek vendor prefix and board compatible, updates
> the Motorcomm PHY driver to consume an optional external PHY reference
> clock, adds missing RV1126 SoC description pieces, and finally adds the
> CLRV1126F core module and DLRV1126 carrier board device trees.
>
> [...]
Applied, thanks!
[1/5] dt-bindings: vendor-prefixes: add alientek
commit: 068a6bd57167cb97058b08f22db0584fabdaab95
[2/5] dt-bindings: arm: rockchip: Add Alientek DLRV1126
commit: f1522feb72aa5231532c9f69e7f5cc91972697e1
[4/5] ARM: dts: rockchip: Add RV1126 I2C5
commit: 17bee9ee734e56d512ec7803eb7948adb56d1d9c
[5/5] ARM: dts: rockchip: Add Alientek DLRV1126
commit: 44045aa7a9a9e6484617f44f77a3efb2d8e78565
Please double-check the sorting on future patches :-) .
The dtsi was perfect, but I did some re-sorting on the carrier-dts.
Best regards,
--
Heiko Stuebner <heiko@sntech.de>
^ permalink raw reply
* [PATCH net-next] net: mdio: Kconfig: Group mdio controller drivers in a submenu
From: Maxime Chevallier @ 2026-07-12 20:17 UTC (permalink / raw)
To: Andrew Lunn, davem, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Russell King, Heiner Kallweit
Cc: Maxime Chevallier, netdev, linux-kernel, thomas.petazzoni
Currently in menuconfig, all individual drivers for MDIO bus controllers
are directly listed under Device drivers -> Network device support.
Let's group them altogether in a submenu, while keeping the dependency
on PHYLIB.
All options under the new submenu match the drivers and infrastructure
listed under drivers/net/mdio/Kconfig.
No intended functional change besides the menuconfig ordering.
Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
---
drivers/net/mdio/Kconfig | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/drivers/net/mdio/Kconfig b/drivers/net/mdio/Kconfig
index e57121019153..b85c9fff9345 100644
--- a/drivers/net/mdio/Kconfig
+++ b/drivers/net/mdio/Kconfig
@@ -3,7 +3,8 @@
# MDIO Layer Configuration
#
-if PHYLIB
+menu "MDIO controller drivers"
+ depends on PHYLIB
config FWNODE_MDIO
def_tristate (ACPI || OF) || COMPILE_TEST
@@ -289,4 +290,4 @@ config MDIO_BUS_MUX_MMIOREG
Currently, only 8/16/32 bits registers are supported.
-endif
+endmenu
--
2.55.0
^ permalink raw reply related
* [net-next v5 4/4] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
From: Jori Koolstra @ 2026-07-12 19:29 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
In-Reply-To: <20260712192958.1631672-1-jkoolstra@xs4all.nl>
Tests SCM_RIGHTS fd passing on a socket with the new socket option
SO_RIGHTS_NOTRUNC turned on. To hook into the security_file_receive()
call, BPF is used. The BPF program shares a hashmap with userspace that
lists the inos to be blocked (of the receiver tgid).
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
---
.../testing/selftests/net/af_unix/.gitignore | 2 +
tools/testing/selftests/net/af_unix/Makefile | 8 +
tools/testing/selftests/net/af_unix/config | 7 +
.../net/af_unix/scm_rights_denial_lsm.bpf.c | 36 +++
.../net/af_unix/scm_rights_denial_lsm.c | 285 ++++++++++++++++++
5 files changed, 338 insertions(+)
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore
index 240b26740c9e..5034482f8864 100644
--- a/tools/testing/selftests/net/af_unix/.gitignore
+++ b/tools/testing/selftests/net/af_unix/.gitignore
@@ -3,6 +3,8 @@ msg_oob
scm_inq
scm_pidfd
scm_rights
+scm_rights_denial_lsm
+scm_rights_denial_lsm.bpf.o
so_peek_off
unix_connect
unix_connreset
diff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile
index 4c0375e28bbe..594cd26ec398 100644
--- a/tools/testing/selftests/net/af_unix/Makefile
+++ b/tools/testing/selftests/net/af_unix/Makefile
@@ -11,9 +11,17 @@ TEST_GEN_PROGS := \
scm_inq \
scm_pidfd \
scm_rights \
+ scm_rights_denial_lsm \
so_peek_off \
unix_connect \
unix_connreset \
# end of TEST_GEN_PROGS
+TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o
+
include ../../lib.mk
+include ../bpf.mk
+
+$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ)
+$(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include
+$(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lz
diff --git a/tools/testing/selftests/net/af_unix/config b/tools/testing/selftests/net/af_unix/config
index b5429c15a53c..acec3f94937e 100644
--- a/tools/testing/selftests/net/af_unix/config
+++ b/tools/testing/selftests/net/af_unix/config
@@ -1,3 +1,10 @@
CONFIG_AF_UNIX_OOB=y
CONFIG_UNIX=y
CONFIG_UNIX_DIAG=m
+CONFIG_BPF=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_BPF_EVENTS=y
+CONFIG_BPF_JIT=y
+CONFIG_SECURITY=y
+CONFIG_BPF_LSM=y
+CONFIG_DEBUG_INFO_BTF=y
diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
new file mode 100644
index 000000000000..4f2414465bfd
--- /dev/null
+++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <linux/errno.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+struct inode {
+ unsigned long i_ino;
+} __attribute__((preserve_access_index));
+
+struct file {
+ struct inode *f_inode;
+} __attribute__((preserve_access_index));
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 16);
+ __type(key, __u64); /* inode number */
+ __type(value, __u32); /* tgid of the receiver being tested */
+} denied_inodes SEC(".maps");
+
+SEC("lsm/file_receive")
+int BPF_PROG(scm_rights_deny, struct file *file)
+{
+ __u32 tgid = bpf_get_current_pid_tgid() >> 32;
+ __u64 ino = file->f_inode->i_ino;
+ __u32 *owner;
+
+ owner = bpf_map_lookup_elem(&denied_inodes, &ino);
+ if (owner && *owner == tgid)
+ return -EPERM;
+
+ return 0;
+}
diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
new file mode 100644
index 000000000000..941b7decf798
--- /dev/null
+++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <bpf/bpf.h>
+#include <bpf/libbpf.h>
+
+#include "kselftest_harness.h"
+
+#ifndef SO_RIGHTS_NOTRUNC
+#define SO_RIGHTS_NOTRUNC 85
+#endif
+
+#define NR_FILES 2
+
+/* Per-file content, so a received fd can be matched to the file sent */
+#define SECRET(n) "secret %d", (n)
+
+/* Indices into the socketpair */
+#define SK_SENDER 0
+#define SK_RECEIVER 1
+
+FIXTURE(scm_rights_denial_bpf)
+{
+ struct bpf_object *obj;
+ struct bpf_link *link;
+ int map_fd;
+ int sk[2];
+ int files[NR_FILES];
+ __u64 inos[NR_FILES];
+ char paths[NR_FILES][64];
+};
+
+FIXTURE_VARIANT(scm_rights_denial_bpf)
+{
+ int sock_type;
+};
+
+FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, stream)
+{
+ .sock_type = SOCK_STREAM,
+};
+
+FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, dgram)
+{
+ .sock_type = SOCK_DGRAM,
+};
+
+FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, seqpacket)
+{
+ .sock_type = SOCK_SEQPACKET,
+};
+
+FIXTURE_SETUP(scm_rights_denial_bpf)
+{
+ struct bpf_program *prog;
+ char lsms[256] = {};
+ int i, fd;
+
+ if (geteuid() != 0)
+ SKIP(return, "requires root");
+
+ fd = open("/sys/kernel/security/lsm", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_LT(0, read(fd, lsms, sizeof(lsms) - 1));
+ close(fd);
+
+ if (!strstr(lsms, "bpf"))
+ SKIP(return, "BPF LSM not active (boot with lsm=...,bpf)");
+
+ self->obj = bpf_object__open_file("scm_rights_denial_lsm.bpf.o", NULL);
+ ASSERT_NE(NULL, self->obj);
+ ASSERT_EQ(0, bpf_object__load(self->obj));
+
+ prog = bpf_object__find_program_by_name(self->obj, "scm_rights_deny");
+ ASSERT_NE(NULL, prog);
+
+ self->link = bpf_program__attach_lsm(prog);
+ ASSERT_NE(NULL, self->link);
+
+ self->map_fd = bpf_object__find_map_fd_by_name(self->obj,
+ "denied_inodes");
+ ASSERT_GE(self->map_fd, 0);
+
+ ASSERT_EQ(0, socketpair(AF_UNIX, variant->sock_type, 0, self->sk));
+
+ for (i = 0; i < NR_FILES; i++) {
+ struct stat st;
+
+ snprintf(self->paths[i], sizeof(self->paths[i]),
+ "/tmp/scm_rights_denial_bpf.%d.XXXXXX", i);
+ self->files[i] = mkstemp(self->paths[i]);
+ ASSERT_GE(self->files[i], 0);
+
+ ASSERT_LT(0, dprintf(self->files[i], SECRET(i)));
+
+ ASSERT_EQ(0, fstat(self->files[i], &st));
+ self->inos[i] = st.st_ino;
+ }
+}
+
+FIXTURE_TEARDOWN(scm_rights_denial_bpf)
+{
+ bpf_link__destroy(self->link);
+ bpf_object__close(self->obj);
+
+ for (int i = 0; i < NR_FILES; i++) {
+ if (self->files[i] >= 0) {
+ close(self->files[i]);
+ unlink(self->paths[i]);
+ }
+ }
+
+ close(self->sk[SK_SENDER]);
+ close(self->sk[SK_RECEIVER]);
+}
+
+static int deny_inode(int map_fd, __u64 ino)
+{
+ __u32 tgid = getpid();
+
+ return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY);
+}
+
+static int set_notrunc(int sk)
+{
+ int one = 1;
+
+ return setsockopt(sk, SOL_SOCKET, SO_RIGHTS_NOTRUNC,
+ &one, sizeof(one));
+}
+
+static int send_fds(int sk, int *fds, int n)
+{
+ char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))] = {};
+ char data = 'x';
+ struct iovec iov = {
+ .iov_base = &data,
+ .iov_len = sizeof(data),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = ctrl,
+ .msg_controllen = CMSG_SPACE(n * sizeof(int)),
+ };
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));
+ memcpy(CMSG_DATA(cmsg), fds, n * sizeof(int));
+
+ return sendmsg(sk, &msg, 0);
+}
+
+static int recv_fd_slots(int sk, int *slots, int *msg_flags)
+{
+ int nr_slots;
+ char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))];
+ char data;
+ struct iovec iov = {
+ .iov_base = &data,
+ .iov_len = sizeof(data),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = ctrl,
+ .msg_controllen = sizeof(ctrl),
+ };
+ struct cmsghdr *cmsg;
+
+ if (recvmsg(sk, &msg, 0) < 0)
+ return -1;
+
+ *msg_flags = msg.msg_flags;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!cmsg)
+ return 0;
+
+ nr_slots = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+ memcpy(slots, CMSG_DATA(cmsg), nr_slots * sizeof(int));
+
+ return nr_slots;
+}
+
+/* Prove a received fd works by reading back the file's content. */
+static int check_secret(int fd, int idx)
+{
+ char want[32], got[32] = {};
+
+ snprintf(want, sizeof(want), SECRET(idx));
+ if (pread(fd, got, sizeof(got) - 1, 0) < 0)
+ return -1;
+
+ return strcmp(want, got);
+}
+
+TEST_F(scm_rights_denial_bpf, all_allowed)
+{
+ int slots[NR_FILES], nr_slots, flags, i;
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+
+ for (i = 0; i < NR_FILES; i++) {
+ ASSERT_GE(slots[i], 0);
+ EXPECT_EQ(0, check_secret(slots[i], i));
+ close(slots[i]);
+ }
+}
+
+TEST_F(scm_rights_denial_bpf, first_denied)
+{
+ int slots[NR_FILES], nr_slots, flags;
+
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[0]));
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+ EXPECT_EQ(-EPERM, slots[0]);
+
+ ASSERT_GE(slots[1], 0);
+ EXPECT_EQ(0, check_secret(slots[1], 1));
+ close(slots[1]);
+}
+
+TEST_F(scm_rights_denial_bpf, all_denied)
+{
+ int slots[NR_FILES], nr_slots, flags, i;
+
+ for (i = 0; i < NR_FILES; i++)
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[i]));
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+
+ for (i = 0; i < NR_FILES; i++)
+ EXPECT_EQ(-EPERM, slots[i]);
+}
+
+TEST_F(scm_rights_denial_bpf, denied_without_notrunc)
+{
+ int slots[NR_FILES], nr_slots, flags;
+
+ /*
+ * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is
+ * truncated at the first denied fd and MSG_CTRUNC is set.
+ */
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1]));
+
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(1, nr_slots);
+ EXPECT_NE(0, flags & MSG_CTRUNC);
+
+ ASSERT_GE(slots[0], 0);
+ EXPECT_EQ(0, check_secret(slots[0], 0));
+ close(slots[0]);
+}
+
+TEST_HARNESS_MAIN
--
2.55.0
^ permalink raw reply related
* [net-next v5 3/4] net: af_unix: useful handling of LSM denials on SCM_RIGHTS
From: Jori Koolstra @ 2026-07-12 19:29 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
In-Reply-To: <20260712192958.1631672-1-jkoolstra@xs4all.nl>
Right now if some LSM such as Smack denies an AF_UNIX socket peer to
receive an SCM_RIGHTS fd, the SCM_RIGHTS fd array will be cut short at
that point, and MSG_CTRUNC is set on return of recvmsg(). This is
highly problematic behaviour, because it leaves the receiver
wondering what happened. As per man page MSG_CTRUNC is supposed to
indicate that the control buffer was sized too short, but suddenly
a permission error might result in the exact same flag being set.
Moreover, the receiver has no chance to determine how many fds got
originally sent and how many were suppressed.[1]
Add a SO_RIGHTS_NOTRUNC option to UNIX sockets to enable more useful
handling of LSM denials when receiving SCM_RIGHTS messages: instead of
truncating the message at the first blocked fd, keep every fd slot
and store the LSM errno in the blocked slot.
[1]: https://github.com/uapi-group/kernel-features#useful-handling-of-lsm-denials-on-scm_rights
Reviewed-by: Christian Brauner (Amutable) <brauner@kernel.org>
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
---
arch/alpha/include/uapi/asm/socket.h | 2 ++
arch/mips/include/uapi/asm/socket.h | 2 ++
arch/parisc/include/uapi/asm/socket.h | 2 ++
arch/sparc/include/uapi/asm/socket.h | 2 ++
include/net/af_unix.h | 1 +
include/net/scm.h | 13 +++------
include/uapi/asm-generic/socket.h | 2 ++
net/compat.c | 4 +--
net/core/scm.c | 38 +++++++++++++++++++++++----
net/unix/af_unix.c | 9 +++++++
10 files changed, 59 insertions(+), 16 deletions(-)
diff --git a/arch/alpha/include/uapi/asm/socket.h b/arch/alpha/include/uapi/asm/socket.h
index 5ef57f88df6b..946a5fad2691 100644
--- a/arch/alpha/include/uapi/asm/socket.h
+++ b/arch/alpha/include/uapi/asm/socket.h
@@ -155,6 +155,8 @@
#define SO_INQ 84
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 85
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64
diff --git a/arch/mips/include/uapi/asm/socket.h b/arch/mips/include/uapi/asm/socket.h
index 72fb1b006da9..f1641dde135f 100644
--- a/arch/mips/include/uapi/asm/socket.h
+++ b/arch/mips/include/uapi/asm/socket.h
@@ -166,6 +166,8 @@
#define SO_INQ 84
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 85
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64
diff --git a/arch/parisc/include/uapi/asm/socket.h b/arch/parisc/include/uapi/asm/socket.h
index c16ec36dfee6..f3a3815c7dc2 100644
--- a/arch/parisc/include/uapi/asm/socket.h
+++ b/arch/parisc/include/uapi/asm/socket.h
@@ -147,6 +147,8 @@
#define SO_INQ 0x4052
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 0x4053
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64
diff --git a/arch/sparc/include/uapi/asm/socket.h b/arch/sparc/include/uapi/asm/socket.h
index 71befa109e1c..7907f3b1f0ee 100644
--- a/arch/sparc/include/uapi/asm/socket.h
+++ b/arch/sparc/include/uapi/asm/socket.h
@@ -148,6 +148,8 @@
#define SO_INQ 0x005d
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 0x005e
+
#if !defined(__KERNEL__)
diff --git a/include/net/af_unix.h b/include/net/af_unix.h
index 34f53dde65ce..bb1b3dee02e8 100644
--- a/include/net/af_unix.h
+++ b/include/net/af_unix.h
@@ -49,6 +49,7 @@ struct unix_sock {
struct scm_stat scm_stat;
int inq_len;
bool recvmsg_inq;
+ bool scm_rights_notrunc;
#if IS_ENABLED(CONFIG_AF_UNIX_OOB)
struct sk_buff *oob_skb;
#endif
diff --git a/include/net/scm.h b/include/net/scm.h
index c52519669349..86ae6bc109ec 100644
--- a/include/net/scm.h
+++ b/include/net/scm.h
@@ -50,8 +50,8 @@ struct scm_cookie {
#endif
};
-void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm);
-void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm);
+void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm, bool notrunc);
+void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm, bool notrunc);
int __scm_send(struct socket *sock, struct msghdr *msg, struct scm_cookie *scm);
void __scm_destroy(struct scm_cookie *scm);
struct scm_fp_list *scm_fp_dup(struct scm_fp_list *fpl);
@@ -107,13 +107,8 @@ void scm_recv(struct socket *sock, struct msghdr *msg,
void scm_recv_unix(struct socket *sock, struct msghdr *msg,
struct scm_cookie *scm, int flags);
-static inline int scm_recv_one_fd(struct file *f, int __user *ufd,
- unsigned int flags)
-{
- if (!ufd)
- return -EFAULT;
- return receive_fd(f, ufd, flags);
-}
+int scm_recv_one_fd(struct file *f, int __user *ufd, unsigned int flags,
+ bool notrunc);
#endif /* __LINUX_NET_SCM_H */
diff --git a/include/uapi/asm-generic/socket.h b/include/uapi/asm-generic/socket.h
index 53b5a8c002b1..84ea7b92936e 100644
--- a/include/uapi/asm-generic/socket.h
+++ b/include/uapi/asm-generic/socket.h
@@ -150,6 +150,8 @@
#define SO_INQ 84
#define SCM_INQ SO_INQ
+#define SO_RIGHTS_NOTRUNC 85
+
#if !defined(__KERNEL__)
#if __BITS_PER_LONG == 64 || (defined(__x86_64__) && defined(__ILP32__))
diff --git a/net/compat.c b/net/compat.c
index d68cf9c3aad5..6bdf4a2c9077 100644
--- a/net/compat.c
+++ b/net/compat.c
@@ -286,7 +286,7 @@ static int scm_max_fds_compat(struct msghdr *msg)
return (msg->msg_controllen - sizeof(struct compat_cmsghdr)) / sizeof(int);
}
-void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm)
+void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm, bool notrunc)
{
struct compat_cmsghdr __user *cm =
(struct compat_cmsghdr __user *)msg->msg_control_user;
@@ -296,7 +296,7 @@ void scm_detach_fds_compat(struct msghdr *msg, struct scm_cookie *scm)
int err = 0, i;
for (i = 0; i < fdmax; i++) {
- err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
+ err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, notrunc);
if (err < 0)
break;
}
diff --git a/net/core/scm.c b/net/core/scm.c
index a73b1eb30fd2..f0d44ecdb11f 100644
--- a/net/core/scm.c
+++ b/net/core/scm.c
@@ -351,7 +351,31 @@ static int scm_max_fds(struct msghdr *msg)
return (msg->msg_controllen - sizeof(struct cmsghdr)) / sizeof(int);
}
-void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
+int scm_recv_one_fd(struct file *f, int __user *ufd, unsigned int flags,
+ bool notrunc)
+{
+ int error;
+
+ if (!ufd)
+ return -EFAULT;
+
+ error = security_file_receive(f);
+ if (error)
+ return notrunc ? put_user(error, ufd) : error;
+
+ FD_PREPARE(fdf, flags, get_file(f));
+ if (fdf.err)
+ return fdf.err;
+
+ error = put_user(fd_prepare_fd(fdf), ufd);
+ if (error)
+ return error;
+
+ __receive_sock(fd_prepare_file(fdf));
+ return fd_publish(fdf);
+}
+
+void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm, bool notrunc)
{
struct cmsghdr __user *cm =
(__force struct cmsghdr __user *)msg->msg_control_user;
@@ -365,12 +389,12 @@ void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
return;
if (msg->msg_flags & MSG_CMSG_COMPAT) {
- scm_detach_fds_compat(msg, scm);
+ scm_detach_fds_compat(msg, scm, notrunc);
return;
}
for (i = 0; i < fdmax; i++) {
- err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags);
+ err = scm_recv_one_fd(scm->fp->fp[i], cmsg_data + i, o_flags, notrunc);
if (err < 0)
break;
}
@@ -542,8 +566,12 @@ void scm_recv_unix(struct socket *sock, struct msghdr *msg,
if (!__scm_recv_common(sock->sk, msg, scm, flags))
return;
- if (scm->fp)
- scm_detach_fds(msg, scm);
+ if (scm->fp) {
+ struct unix_sock *u;
+
+ u = unix_sk(sock->sk);
+ scm_detach_fds(msg, scm, READ_ONCE(u->scm_rights_notrunc));
+ }
if (sock->sk->sk_scm_pidfd)
scm_pidfd_recv(msg, scm);
diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c
index 3d256255085b..e7b3aab2f176 100644
--- a/net/unix/af_unix.c
+++ b/net/unix/af_unix.c
@@ -921,6 +921,7 @@ static bool unix_custom_sockopt(int optname)
{
switch (optname) {
case SO_INQ:
+ case SO_RIGHTS_NOTRUNC:
return true;
default:
return false;
@@ -956,6 +957,14 @@ static int unix_setsockopt(struct socket *sock, int level, int optname,
WRITE_ONCE(u->recvmsg_inq, val);
break;
+
+ case SO_RIGHTS_NOTRUNC:
+ if (val > 1 || val < 0)
+ return -EINVAL;
+
+ WRITE_ONCE(u->scm_rights_notrunc, val);
+ break;
+
default:
return -ENOPROTOOPT;
}
--
2.55.0
^ permalink raw reply related
* [net-next v5 2/4] net: scm: move scm_detach_fds() from common path to scm_recv_unix()
From: Jori Koolstra @ 2026-07-12 19:29 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
In-Reply-To: <20260712192958.1631672-1-jkoolstra@xs4all.nl>
scm->fp can only be set when using UNIX sockets, therefore we should
move it out of the common path __scm_recv_common() into
scm_recv_unix().
Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
---
net/core/scm.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/net/core/scm.c b/net/core/scm.c
index eec13f50ecaf..a73b1eb30fd2 100644
--- a/net/core/scm.c
+++ b/net/core/scm.c
@@ -523,9 +523,6 @@ static bool __scm_recv_common(struct sock *sk, struct msghdr *msg,
scm_passec(sk, msg, scm);
- if (scm->fp)
- scm_detach_fds(msg, scm);
-
return true;
}
@@ -545,6 +542,9 @@ void scm_recv_unix(struct socket *sock, struct msghdr *msg,
if (!__scm_recv_common(sock->sk, msg, scm, flags))
return;
+ if (scm->fp)
+ scm_detach_fds(msg, scm);
+
if (sock->sk->sk_scm_pidfd)
scm_pidfd_recv(msg, scm);
--
2.55.0
^ permalink raw reply related
* [net-next v5 1/4] net: af_unix: enable custom setsockopt for all socket types
From: Jori Koolstra @ 2026-07-12 19:29 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
In-Reply-To: <20260712192958.1631672-1-jkoolstra@xs4all.nl>
unix_setsockopt() and the SOCK_CUSTOM_SOCKOPT flag were only wired up
for SOCK_STREAM (introduced along with the stream-only SO_INQ).
Consequently custom AF_UNIX options are unreachable on SOCK_DGRAM and
SOCK_SEQPACKET: those setsockopt() calls bypass unix_setsockopt() and
fall through to the generic sock_setsockopt(), failing with
-ENOPROTOOPT.
Set SOCK_CUSTOM_SOCKOPT for every AF_UNIX socket type in unix_create(), and
also for accepted sockets in unix_accept() (reachable for stream and
seqpacket).
This is a prerequisite for making SO_RIGHTS_NOTRUNC settable on all AF_UNIX
socket types.
Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl>
---
net/unix/af_unix.c | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c
index f7a9d55eee8a..3d256255085b 100644
--- a/net/unix/af_unix.c
+++ b/net/unix/af_unix.c
@@ -949,7 +949,7 @@ static int unix_setsockopt(struct socket *sock, int level, int optname,
switch (optname) {
case SO_INQ:
if (sk->sk_type != SOCK_STREAM)
- return -EINVAL;
+ return -ENOPROTOOPT;
if (val > 1 || val < 0)
return -EINVAL;
@@ -1005,6 +1005,7 @@ static const struct proto_ops unix_dgram_ops = {
#endif
.listen = sock_no_listen,
.shutdown = unix_shutdown,
+ .setsockopt = unix_setsockopt,
.sendmsg = unix_dgram_sendmsg,
.read_skb = unix_read_skb,
.recvmsg = unix_dgram_recvmsg,
@@ -1029,6 +1030,7 @@ static const struct proto_ops unix_seqpacket_ops = {
#endif
.listen = unix_listen,
.shutdown = unix_shutdown,
+ .setsockopt = unix_setsockopt,
.sendmsg = unix_seqpacket_sendmsg,
.recvmsg = unix_seqpacket_recvmsg,
.mmap = sock_no_mmap,
@@ -1142,9 +1144,10 @@ static int unix_create(struct net *net, struct socket *sock, int protocol,
if (protocol && protocol != PF_UNIX)
return -EPROTONOSUPPORT;
+ set_bit(SOCK_CUSTOM_SOCKOPT, &sock->flags);
+
switch (sock->type) {
case SOCK_STREAM:
- set_bit(SOCK_CUSTOM_SOCKOPT, &sock->flags);
sock->ops = &unix_stream_ops;
break;
/*
@@ -1864,8 +1867,7 @@ static int unix_accept(struct socket *sock, struct socket *newsock,
skb_free_datagram(sk, skb);
wake_up_interruptible(&unix_sk(sk)->peer_wait);
- if (tsk->sk_type == SOCK_STREAM)
- set_bit(SOCK_CUSTOM_SOCKOPT, &newsock->flags);
+ set_bit(SOCK_CUSTOM_SOCKOPT, &newsock->flags);
/* attach accepted sock to socket */
unix_state_lock(tsk);
--
2.55.0
^ permalink raw reply related
* [net-next v5 0/4] net: af_unix: useful handling of LSM denials on SCM_RIGHTS
From: Jori Koolstra @ 2026-07-12 19:29 UTC (permalink / raw)
To: Christian Brauner, Aleksa Sarai, Kuniyuki Iwashima,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman
Cc: netdev, linux-fsdevel, linux-kernel, Jori Koolstra
Right now if some LSM denies an AF_UNIX socket peer to receive a
SCM_RIGHTS fd, the SCM_RIGHTS fd array will be cut short at
that point, and MSG_CTRUNC is set on return of recvmsg(2). This is
highly problematic behaviour, because it leaves the receiver
wondering what happened. As per man page MSG_CTRUNC is supposed to
indicate that the control buffer was sized too short, but suddenly
a permission error might result in the exact same flag being set.
Moreover, the receiver has no chance to determine how many fds got
originally sent and how many were suppressed.[1]
Add a SO_RIGHTS_NOTRUNC option to UNIX sockets to enable more useful
handling of LSM denials when receiving SCM_RIGHTS messages: instead of
truncating the message at the first blocked fd, keep every fd slot
and store the LSM errno in the blocked slot.
[1]: https://github.com/uapi-group/kernel-features#useful-handling-of-lsm-denials-on-scm_rights
Changes:
v5:
- Enable SO_RIGHTS_NOTRUNC on all AF_UNIX socket types.
- Added required BPF CONFIG_ options to
tools/testing/selftests/net/af_unix/config.
v4: https://lore.kernel.org/netdev/20260705123826.3818443-1-jkoolstra@xs4all.nl/
- Removed the __receive_fd() helper and moved logic into
scm_recv_one_fd() directly (suggested by Brauner).
- Moved selftest from Smack to BPF (LLM assisted).
- Add arch specific socket option values for SO_RIGHTS_NOTRUNC.
- Undo patch that replaced copy_from_sockptr() with
copy_safe_from_sockptr().
v3:
- Separated net and vfs changes.
- Use kselftest_harness.h and system() to call the test script.
v2: https://lore.kernel.org/netdev/20260616143020.3458085-2-jkoolstra@xs4all.nl/
- Reimplemented as a UNIX socket option instead of a per recvmsg(2) flag.
v1: https://lore.kernel.org/netdev/20260428175125.2705296-1-jkoolstra@xs4all.nl/
Jori Koolstra (4):
net: af_unix: enable custom setsockopt for all socket types
net: scm: move scm_detach_fds() from common path to scm_recv_unix()
net: af_unix: useful handling of LSM denials on SCM_RIGHTS
selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
arch/alpha/include/uapi/asm/socket.h | 2 +
arch/mips/include/uapi/asm/socket.h | 2 +
arch/parisc/include/uapi/asm/socket.h | 2 +
arch/sparc/include/uapi/asm/socket.h | 2 +
include/net/af_unix.h | 1 +
include/net/scm.h | 13 +-
include/uapi/asm-generic/socket.h | 2 +
net/compat.c | 4 +-
net/core/scm.c | 40 ++-
net/unix/af_unix.c | 19 +-
.../testing/selftests/net/af_unix/.gitignore | 2 +
tools/testing/selftests/net/af_unix/Makefile | 8 +
tools/testing/selftests/net/af_unix/config | 7 +
.../net/af_unix/scm_rights_denial_lsm.bpf.c | 36 +++
.../net/af_unix/scm_rights_denial_lsm.c | 285 ++++++++++++++++++
15 files changed, 404 insertions(+), 21 deletions(-)
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c
create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
base-commit: f6f3b36c15ed44de1fbb44e645e4fae8c4a4453e
--
2.55.0
^ permalink raw reply
* [PATCH net-next v7 4/4] net: pse-pd: realtek-pse-mcu: add UART transport
From: Jonas Jelonek @ 2026-07-12 19:22 UTC (permalink / raw)
To: Oleksij Rempel, Kory Maincent, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
Jonas Jelonek
In-Reply-To: <20260712192251.1413279-1-jelonek.jonas@gmail.com>
Add the serdev (UART) transport for the Realtek PSE MCU core. It registers
the MCU as a serdev device and provides the send/recv callbacks the core
uses to exchange the 12-byte frames, receiving asynchronously via the
serdev receive_buf callback.
The baud rate defaults to 19200 and can be overridden per board with the
"current-speed" property.
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
drivers/net/pse-pd/Kconfig | 11 ++
drivers/net/pse-pd/Makefile | 1 +
drivers/net/pse-pd/realtek-pse-mcu-uart.c | 155 ++++++++++++++++++++++
3 files changed, 167 insertions(+)
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-uart.c
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 6d14c8832e8b..a0f2ae668c67 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -30,6 +30,17 @@ config PSE_REALTEK_MCU_I2C
PSE silicon is not accessed directly. To compile this driver as a
module, choose M here: the module will be called realtek-pse-mcu-i2c.
+config PSE_REALTEK_MCU_UART
+ tristate "Realtek PSE MCU driver (UART transport)"
+ depends on SERIAL_DEV_BUS
+ select PSE_REALTEK_MCU
+ help
+ Driver for the microcontroller (MCU) that fronts the PSE
+ hardware on various Realtek-based managed switches, attached
+ via UART. The MCU exposes a message-based protocol; the actual PSE
+ silicon is not accessed directly. To compile this driver as a
+ module, choose M here: the module will be called realtek-pse-mcu-uart.
+
config PSE_REGULATOR
tristate "Regulator based PSE controller"
help
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index ef869bba5ed9..9cca5900fe34 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -5,6 +5,7 @@ obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o
+obj-$(CONFIG_PSE_REALTEK_MCU_UART) += realtek-pse-mcu-uart.o
obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-uart.c b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
new file mode 100644
index 000000000000..91cadb5569ab
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/serdev.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+
+#include "realtek-pse-mcu.h"
+
+#define RTPSE_MCU_UART_BAUD_DEFAULT 19200
+#define RTPSE_MCU_UART_TX_TIMEOUT msecs_to_jiffies(100)
+#define RTPSE_MCU_UART_RX_TIMEOUT msecs_to_jiffies(RTPSE_MCU_RESPONSE_MAX_MS)
+
+struct rtpse_mcu_uart {
+ struct rtpse_mcu_ctrl pse;
+ struct serdev_device *serdev;
+ struct completion rx_done;
+ spinlock_t rx_lock; /* protects rx_buf and rx_len */
+ size_t rx_len;
+ u8 rx_buf[RTPSE_MCU_MSG_SIZE];
+};
+
+#define to_rtpse_mcu_uart(p) container_of(p, struct rtpse_mcu_uart, pse)
+
+/*
+ * No framing is done here: a glitched frame costs one transaction, then
+ * the next _send re-frames from rx_len 0. Resync works by returning count
+ * (not take), dropping any overflow so serdev keeps no leftover to bleed
+ * into the next frame.
+ */
+static size_t rtpse_mcu_uart_receive(struct serdev_device *serdev,
+ const u8 *buf, size_t count)
+{
+ struct rtpse_mcu_uart *ctx = serdev_device_get_drvdata(serdev);
+ size_t take;
+
+ scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+ take = min(count, sizeof(ctx->rx_buf) - ctx->rx_len);
+ if (take) {
+ memcpy(ctx->rx_buf + ctx->rx_len, buf, take);
+ ctx->rx_len += take;
+ if (ctx->rx_len == sizeof(ctx->rx_buf))
+ complete(&ctx->rx_done);
+ }
+ }
+
+ /* consume all to avoid desync/misalignment */
+ return count;
+}
+
+static const struct serdev_device_ops rtpse_mcu_uart_serdev_ops = {
+ .receive_buf = rtpse_mcu_uart_receive,
+ .write_wakeup = serdev_device_write_wakeup,
+};
+
+static int rtpse_mcu_uart_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+ struct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);
+ int written;
+
+ /* clear any leftover rx state before transmitting */
+ scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+ reinit_completion(&ctx->rx_done);
+ ctx->rx_len = 0;
+ }
+
+ written = serdev_device_write(ctx->serdev, (const u8 *)req, sizeof(*req),
+ RTPSE_MCU_UART_TX_TIMEOUT);
+ if (written < 0)
+ return written;
+ if (written != sizeof(*req))
+ return -EIO;
+
+ return 0;
+}
+
+static int rtpse_mcu_uart_recv(struct rtpse_mcu_ctrl *pse,
+ const struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp)
+{
+ struct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);
+
+ if (!wait_for_completion_timeout(&ctx->rx_done, RTPSE_MCU_UART_RX_TIMEOUT))
+ return -ETIMEDOUT;
+
+ scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+ if (ctx->rx_len != sizeof(*resp))
+ return -EIO;
+
+ memcpy(resp, ctx->rx_buf, sizeof(*resp));
+ }
+ return 0;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_uart_transport_ops = {
+ .send = rtpse_mcu_uart_send,
+ .recv = rtpse_mcu_uart_recv,
+};
+
+static int rtpse_mcu_uart_probe(struct serdev_device *serdev)
+{
+ u32 speed = RTPSE_MCU_UART_BAUD_DEFAULT;
+ struct device *dev = &serdev->dev;
+ struct rtpse_mcu_uart *ctx;
+ int ret;
+
+ ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
+ if (!ctx)
+ return -ENOMEM;
+
+ ctx->serdev = serdev;
+ ctx->pse.dev = dev;
+ ctx->pse.pcdev.owner = THIS_MODULE;
+ ctx->pse.transport = &rtpse_mcu_uart_transport_ops;
+ init_completion(&ctx->rx_done);
+ spin_lock_init(&ctx->rx_lock);
+
+ serdev_device_set_drvdata(serdev, ctx);
+ serdev_device_set_client_ops(serdev, &rtpse_mcu_uart_serdev_ops);
+
+ ret = devm_serdev_device_open(dev, serdev);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to open serdev\n");
+
+ fwnode_property_read_u32(dev_fwnode(dev), "current-speed", &speed);
+ serdev_device_set_baudrate(serdev, speed);
+ serdev_device_set_flow_control(serdev, false);
+ serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
+
+ return rtpse_mcu_register(&ctx->pse);
+}
+
+static const struct of_device_id rtpse_mcu_uart_of_match[] = {
+ { .compatible = "realtek,pse-mcu-gen1", .data = &rtpse_mcu_gen1_data },
+ { .compatible = "realtek,pse-mcu-gen2", .data = &rtpse_mcu_gen2_data },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_mcu_uart_of_match);
+
+static struct serdev_device_driver rtpse_mcu_uart_driver = {
+ .driver = {
+ .name = "realtek-pse-mcu-uart",
+ .of_match_table = rtpse_mcu_uart_of_match,
+ },
+ .probe = rtpse_mcu_uart_probe,
+};
+module_serdev_device_driver(rtpse_mcu_uart_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek PSE MCU driver (UART transport)");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH net-next v7 3/4] net: pse-pd: realtek-pse-mcu: add I2C transport
From: Jonas Jelonek @ 2026-07-12 19:22 UTC (permalink / raw)
To: Oleksij Rempel, Kory Maincent, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
Jonas Jelonek
In-Reply-To: <20260712192251.1413279-1-jelonek.jonas@gmail.com>
Add the I2C/SMBus transport for the Realtek PSE MCU core. It registers
the MCU on an I2C bus and provides the send/recv callbacks the core
uses to exchange the 12-byte frames.
The MCU firmware expects one of two framings on the I2C bus, and which one
is part of the compatible: '-smbus' (reads carry a leading command byte
and a repeated start) or raw '-i2c' (bare block writes and reads). The
match data flags the raw-I2C case; SMBus is the default because that's
what the majority of devices uses.
Because i2c_master_send()/i2c_master_recv() may DMA, the raw-I2C path
bounces each frame through a heap buffer rather than the core's stack
buffers; the SMBus path is unaffected.
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
drivers/net/pse-pd/Kconfig | 11 ++
drivers/net/pse-pd/Makefile | 1 +
drivers/net/pse-pd/realtek-pse-mcu-i2c.c | 170 +++++++++++++++++++++++
3 files changed, 182 insertions(+)
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-i2c.c
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 3b0c245a2bc7..6d14c8832e8b 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -19,6 +19,17 @@ config PSE_REALTEK_MCU
Shared core for the Realtek PSE MCU driver. This is selected
automatically by the transport options below.
+config PSE_REALTEK_MCU_I2C
+ tristate "Realtek PSE MCU driver (I2C transport)"
+ depends on I2C
+ select PSE_REALTEK_MCU
+ help
+ Driver for the microcontroller (MCU) that fronts the PSE
+ hardware on various Realtek-based managed switches, attached
+ via I2C/SMBus. The MCU exposes a message-based protocol; the actual
+ PSE silicon is not accessed directly. To compile this driver as a
+ module, choose M here: the module will be called realtek-pse-mcu-i2c.
+
config PSE_REGULATOR
tristate "Regulator based PSE controller"
help
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index bf35e2a5b110..ef869bba5ed9 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -4,6 +4,7 @@
obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
+obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o
obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-i2c.c b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c
new file mode 100644
index 000000000000..e10f8c633b68
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "realtek-pse-mcu.h"
+
+/*
+ * The core has already waited RTPSE_MCU_RESPONSE_MS before calling us, so
+ * the response is normally ready on the very first read. For commands the
+ * MCU produces more slowly, keep polling at the typical response cadence
+ * up to the worst-case ceiling.
+ */
+#define RTPSE_MCU_I2C_RETRY_MS RTPSE_MCU_RESPONSE_MS
+#define RTPSE_MCU_I2C_MAX_TRIES (RTPSE_MCU_RESPONSE_MAX_MS / RTPSE_MCU_I2C_RETRY_MS)
+
+static int rtpse_mcu_i2c_smbus_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+ struct i2c_client *client = to_i2c_client(pse->dev);
+
+ /* Send opcode as SMBus command byte; remaining 11 bytes as block data */
+ return i2c_smbus_write_i2c_block_data(client, req->opcode, RTPSE_MCU_MSG_SIZE - 1,
+ (u8 *)req + 1);
+}
+
+static int rtpse_mcu_i2c_smbus_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp)
+{
+ struct i2c_client *client = to_i2c_client(pse->dev);
+ int tries, ret;
+
+ for (tries = 0; tries < RTPSE_MCU_I2C_MAX_TRIES; tries++) {
+ if (tries > 0)
+ msleep(RTPSE_MCU_I2C_RETRY_MS);
+
+ /* MCU needs 0x00 as command byte for read */
+ ret = i2c_smbus_read_i2c_block_data(client, 0x00,
+ RTPSE_MCU_MSG_SIZE,
+ (u8 *)resp);
+ if (ret < 0)
+ return ret;
+ if (ret == RTPSE_MCU_MSG_SIZE && rtpse_mcu_resp_is_final(req, resp))
+ return 0;
+ }
+
+ return -ETIMEDOUT;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_smbus_ops = {
+ .send = rtpse_mcu_i2c_smbus_send,
+ .recv = rtpse_mcu_i2c_smbus_recv,
+};
+
+static int rtpse_mcu_i2c_native_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+ struct i2c_client *client = to_i2c_client(pse->dev);
+ void *buf;
+ int ret;
+
+ /* i2c_master_send() may DMA, so the buffer must not be on the stack. */
+ buf = kmemdup(req, RTPSE_MCU_MSG_SIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = i2c_master_send(client, buf, RTPSE_MCU_MSG_SIZE);
+ kfree(buf);
+ if (ret < 0)
+ return ret;
+ return ret == RTPSE_MCU_MSG_SIZE ? 0 : -EIO;
+}
+
+static int rtpse_mcu_i2c_native_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp)
+{
+ struct i2c_client *client = to_i2c_client(pse->dev);
+ int tries, ret;
+ u8 *buf;
+
+ /* i2c_master_recv() may DMA, so read into an off-stack buffer. */
+ buf = kmalloc(RTPSE_MCU_MSG_SIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ for (tries = 0; tries < RTPSE_MCU_I2C_MAX_TRIES; tries++) {
+ if (tries > 0)
+ msleep(RTPSE_MCU_I2C_RETRY_MS);
+
+ ret = i2c_master_recv(client, buf, RTPSE_MCU_MSG_SIZE);
+ if (ret < 0)
+ goto out;
+ if (ret == RTPSE_MCU_MSG_SIZE) {
+ memcpy(resp, buf, RTPSE_MCU_MSG_SIZE);
+ if (rtpse_mcu_resp_is_final(req, resp)) {
+ ret = 0;
+ goto out;
+ }
+ }
+ }
+ ret = -ETIMEDOUT;
+out:
+ kfree(buf);
+ return ret;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_native_ops = {
+ .send = rtpse_mcu_i2c_native_send,
+ .recv = rtpse_mcu_i2c_native_recv,
+};
+
+static int rtpse_mcu_i2c_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ const struct rtpse_mcu_match_data *match;
+ struct rtpse_mcu_ctrl *pse;
+ bool use_native;
+
+ match = device_get_match_data(dev);
+ if (!match)
+ return dev_err_probe(dev, -ENODEV, "missing match data\n");
+
+ /* The framing (raw I2C vs SMBus) is carried by the match data. */
+ use_native = match->native_i2c;
+ if (use_native) {
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+ return dev_err_probe(dev, -EOPNOTSUPP,
+ "plain-I2C MCU protocol requires I2C-capable adapter\n");
+ } else {
+ if (!i2c_check_functionality(client->adapter,
+ I2C_FUNC_SMBUS_WRITE_I2C_BLOCK |
+ I2C_FUNC_SMBUS_READ_I2C_BLOCK))
+ return dev_err_probe(dev, -EOPNOTSUPP,
+ "SMBus MCU protocol requires SMBus I2C-block support\n");
+ }
+
+ pse = devm_kzalloc(dev, sizeof(*pse), GFP_KERNEL);
+ if (!pse)
+ return -ENOMEM;
+
+ pse->dev = dev;
+ pse->pcdev.owner = THIS_MODULE;
+ pse->transport = use_native ? &rtpse_mcu_i2c_native_ops : &rtpse_mcu_i2c_smbus_ops;
+
+ return rtpse_mcu_register(pse);
+}
+
+static const struct of_device_id rtpse_mcu_i2c_of_match[] = {
+ { .compatible = "realtek,pse-mcu-gen1-smbus", .data = &rtpse_mcu_gen1_data },
+ { .compatible = "realtek,pse-mcu-gen2-smbus", .data = &rtpse_mcu_gen2_data },
+ { .compatible = "realtek,pse-mcu-gen2-i2c", .data = &rtpse_mcu_gen2_i2c_data },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_mcu_i2c_of_match);
+
+static struct i2c_driver rtpse_mcu_i2c_driver = {
+ .driver = {
+ .name = "realtek-pse-mcu-i2c",
+ .of_match_table = rtpse_mcu_i2c_of_match,
+ },
+ .probe = rtpse_mcu_i2c_probe,
+};
+module_i2c_driver(rtpse_mcu_i2c_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek PSE MCU driver (I2C transport)");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH net-next v7 2/4] net: pse-pd: add Realtek PSE MCU core
From: Jonas Jelonek @ 2026-07-12 19:22 UTC (permalink / raw)
To: Oleksij Rempel, Kory Maincent, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
Jonas Jelonek
In-Reply-To: <20260712192251.1413279-1-jelonek.jonas@gmail.com>
A range of managed Realtek-based PoE switches use a small microcontroller
on the PCB to front the actual PSE silicon. The host CPU talks to that
MCU over I2C/SMBus or UART using a fixed 12-byte request/response
protocol with a trailing checksum; the PSE chips are managed by the MCU
and are not accessed directly. Two generations of the protocol exist -
both Realtek's - diverging in opcode numbering and a few response
layouts; the driver handles this with a per-dialect opcode table and
parser hooks for the responses that differ, selected by the compatible.
The specific PSE chip behind the MCU is detected at runtime and only
influences per-chip constants (power scaling and the per-port cap).
This core module implements the protocol, message framing, the dialect
machinery and the pse_controller_ops glue, and exports a registration
helper for transport modules. The I2C and UART transports that drive it
follow in the next patches; the core (PSE_REALTEK_MCU) is selected
automatically by those transports and is not user-selectable on its own.
The realtek-pse-mcu-* files and PSE_REALTEK_MCU* symbols match the
realtek,pse-mcu-* compatibles (see the binding for the naming rationale).
The two protocol generations - gen1 on older Broadcom-PSE boards, gen2 on
Realtek's own PSE silicon - are both Realtek's, handled by the same shared
core, each selecting its dialect via the compatible.
Power budgeting is left to the MCU firmware; the driver advertises
PSE_BUDGET_EVAL_STRAT_DYNAMIC (controller-managed budget) accordingly.
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
MAINTAINERS | 7 +
drivers/net/pse-pd/Kconfig | 6 +
drivers/net/pse-pd/Makefile | 1 +
drivers/net/pse-pd/realtek-pse-mcu-core.c | 995 ++++++++++++++++++++++
drivers/net/pse-pd/realtek-pse-mcu.h | 91 ++
5 files changed, 1100 insertions(+)
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-core.c
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu.h
diff --git a/MAINTAINERS b/MAINTAINERS
index f3218abefd0c..d84a4641aaec 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22717,6 +22717,13 @@ S: Maintained
F: include/sound/rt*.h
F: sound/soc/codecs/rt*
+REALTEK PSE MCU DRIVER
+M: Jonas Jelonek <jelonek.jonas@gmail.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
+F: drivers/net/pse-pd/realtek-pse-mcu*
+
REALTEK OTTO WATCHDOG
M: Sander Vanheule <sander@svanheule.net>
L: linux-watchdog@vger.kernel.org
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 7ef29657ee5d..3b0c245a2bc7 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -13,6 +13,12 @@ menuconfig PSE_CONTROLLER
if PSE_CONTROLLER
+config PSE_REALTEK_MCU
+ tristate
+ help
+ Shared core for the Realtek PSE MCU driver. This is selected
+ automatically by the transport options below.
+
config PSE_REGULATOR
tristate "Regulator based PSE controller"
help
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index cc78f7ea7f5f..bf35e2a5b110 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -3,6 +3,7 @@
obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
+obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-core.c b/drivers/net/pse-pd/realtek-pse-mcu-core.c
new file mode 100644
index 000000000000..340493c14e4a
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c
@@ -0,0 +1,995 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for the microcontroller (MCU) fronting PSE silicon on various
+ * Realtek-based managed switches. The MCU speaks a 12-byte fixed-frame
+ * management protocol; this driver covers two generations of the
+ * protocol via a per-dialect opcode table and response parsers.
+ *
+ * Many PoE switch designs put a dedicated microcontroller in front of the
+ * actual PSE silicon: the host CPU talks to the MCU over I2C/SMBus or
+ * UART, and the MCU in turn manages the PSE chips on the board. The MCU
+ * speaks a small message-based protocol. The PSE chips themselves are not
+ * accessed directly; everything goes through MCU commands.
+ *
+ * This driver targets that architecture for the Realtek-family protocol.
+ * Two generations are supported: Gen1 being used on older switches where
+ * the MCU fronts and manages Broadcom PSE silicon; Gen2 being used with
+ * Realtek PSE silicon. The two share frame format and a sum-mod-256
+ * checksum but diverge on opcode numbers and on a few response layouts;
+ * this is handled by the per-dialect opcode table and parser hooks.
+ *
+ * Out of scope: PSE chips that are interfaced directly from the host
+ * without a management MCU, MCU designs that speak an unrelated protocol
+ * family, and "dumb PSE" modes where no host control is wired up at all.
+ *
+ * This core module implements the protocol, decoding/encoding of MCU
+ * responses, and the pse_controller_ops integration. Transport modules
+ * (realtek-pse-mcu-i2c, realtek-pse-mcu-uart) provide the send/recv
+ * callbacks.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/jiffies.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/regulator/consumer.h>
+#include <linux/unaligned.h>
+
+#include "realtek-pse-mcu.h"
+
+#define RTPSE_MCU_DEVICE_ID_RTL8238B 0x0138
+#define RTPSE_MCU_DEVICE_ID_RTL8239 0x0039
+#define RTPSE_MCU_DEVICE_ID_RTL8239C 0x0139
+#define RTPSE_MCU_DEVICE_ID_BCM59111 0xe111
+#define RTPSE_MCU_DEVICE_ID_BCM59121 0xe121
+
+#define RTPSE_MCU_PORT_STS_DISABLED 0x00
+#define RTPSE_MCU_PORT_STS_SEARCHING 0x01
+#define RTPSE_MCU_PORT_STS_DELIVERING 0x02
+#define RTPSE_MCU_PORT_STS_TEST 0x03 /* Gen1-only; reserved on Gen2 */
+#define RTPSE_MCU_PORT_STS_FAULT 0x04
+#define RTPSE_MCU_PORT_STS_OTHER_FAULT 0x05 /* Gen1-only; reserved on Gen2 */
+#define RTPSE_MCU_PORT_STS_REQUESTING 0x06
+
+/* RTPSE_MCU_PORT_SET_POWER_LIMIT_TYPE values */
+#define RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER 0x02
+
+#define RTPSE_MCU_MAX_PORTS 48
+#define RTPSE_MCU_PORT_MAX_PRIORITY 3
+
+enum rtpse_mcu_cmd {
+ RTPSE_MCU_CMD_SET_GLOBAL_STATE,
+ RTPSE_MCU_CMD_GET_SYSTEM_INFO,
+ RTPSE_MCU_CMD_GET_EXT_CONFIG,
+
+ RTPSE_MCU_CMD_PORT_ENABLE,
+ RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE,
+ RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+ RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+ RTPSE_MCU_CMD_PORT_SET_PRIORITY,
+ RTPSE_MCU_CMD_PORT_GET_STATUS,
+ RTPSE_MCU_CMD_PORT_GET_POWER_STATS,
+ RTPSE_MCU_CMD_PORT_GET_CONFIG,
+ RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG,
+
+ RTPSE_MCU_NUM_CMDS,
+};
+
+struct rtpse_mcu_opcode {
+ u8 op;
+ bool valid;
+};
+
+/* Shorthand for the designated-initializer entries in dialect opcode tables. */
+#define RTPSE_MCU_OP(opc) { .op = (opc), .valid = true }
+
+/* Parsed MCU response structures (decoded from rtpse_mcu_msg replies) */
+
+struct rtpse_mcu_info {
+ u8 max_ports;
+ bool system_enable;
+ u16 device_id;
+ u8 sw_ver;
+ u8 mcu_type;
+ u8 config_status;
+ u8 ext_ver;
+};
+
+struct rtpse_mcu_ext_config {
+ u8 uvlo;
+ u8 ovlo;
+ bool prealloc_enable;
+ u8 num_of_pses;
+};
+
+struct rtpse_mcu_port_status {
+ u8 sts1;
+ u8 sts2;
+ u8 sts3;
+};
+
+struct rtpse_mcu_port_measurement {
+ u16 voltage_raw; /* 64.45mV/LSB */
+ u16 current_raw; /* 1mA/LSB */
+ u16 temperature_raw; /* T(mC) = 1250 * (220 - raw) */
+ u16 power_raw; /* 100mW/LSB */
+};
+
+struct rtpse_mcu_port_config {
+ bool enable;
+};
+
+struct rtpse_mcu_port_ext_config {
+ u8 max_power;
+ u8 priority;
+};
+
+struct rtpse_mcu_dialect {
+ struct rtpse_mcu_opcode opcode[RTPSE_MCU_NUM_CMDS];
+
+ /*
+ * Response parsers for the fields that differ between dialects; each
+ * dialect supplies its own. Other responses share one layout and are
+ * decoded directly - a dialect that diverges there must add a hook,
+ * as a mismatched layout cannot be detected (the checksum still passes).
+ */
+ void (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);
+ int (*parse_port_class)(const struct rtpse_mcu_port_status *status);
+ const char *(*mcu_type_str)(unsigned int mcu_type);
+};
+
+struct rtpse_mcu_chip_info {
+ const char *name;
+ u16 device_id;
+ u32 max_mW_per_port;
+ enum rtpse_mcu_cmd pw_set_cmd; /* command used by set_pw_limit */
+ u32 pw_set_lsb_mW; /* LSB of pw_set_cmd value, in mW */
+ u32 pw_read_lsb_mW; /* LSB of ext_config.max_power read-back, in mW */
+};
+
+static const struct rtpse_mcu_chip_info rtl8238b_info = {
+ .device_id = RTPSE_MCU_DEVICE_ID_RTL8238B,
+ .max_mW_per_port = 30000,
+ .name = "RTL8238B",
+ .pw_read_lsb_mW = 200,
+ .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+ .pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_mcu_chip_info rtl8239_info = {
+ .device_id = RTPSE_MCU_DEVICE_ID_RTL8239,
+ .max_mW_per_port = 90000,
+ .name = "RTL8239",
+ .pw_read_lsb_mW = 400,
+ .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+ .pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_mcu_chip_info rtl8239c_info = {
+ .device_id = RTPSE_MCU_DEVICE_ID_RTL8239C,
+ .max_mW_per_port = 90000,
+ .name = "RTL8239C",
+ .pw_read_lsb_mW = 400,
+ .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+ .pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_mcu_chip_info bcm59111_info = {
+ .device_id = RTPSE_MCU_DEVICE_ID_BCM59111,
+ .max_mW_per_port = 30000,
+ .name = "BCM59111",
+ .pw_read_lsb_mW = 200,
+ .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+ .pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_mcu_chip_info bcm59121_info = {
+ .device_id = RTPSE_MCU_DEVICE_ID_BCM59121,
+ /*
+ * BCM59121 is a 60W Type-3 part, but known boards run it at 802.3at
+ * and the Gen1 dialect has only the 8-bit/0.2W set command (<=51W);
+ * cap at the 30W the hardware actually offers.
+ */
+ .max_mW_per_port = 30000,
+ .name = "BCM59121",
+ .pw_read_lsb_mW = 200,
+ .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+ .pw_set_lsb_mW = 200,
+};
+
+/* Helpers and basic functions */
+
+static struct rtpse_mcu_ctrl *to_rtpse_mcu_ctrl(struct pse_controller_dev *pcdev)
+{
+ return container_of(pcdev, struct rtpse_mcu_ctrl, pcdev);
+}
+
+static void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)
+{
+ memset(msg, 0xff, sizeof(*msg));
+ msg->opcode = opcode;
+}
+
+static u8 rtpse_mcu_checksum(const u8 *buf, size_t len)
+{
+ u8 sum = 0;
+
+ while (len--)
+ sum += *buf++;
+ return sum;
+}
+
+static int rtpse_mcu_do_xfer(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp)
+{
+ int ret;
+
+ scoped_guard(mutex, &pse->mutex) {
+ /* Rolling seq_num so a late reply can't pass as a later one. */
+ req->seq_num = pse->seq++;
+ req->checksum = rtpse_mcu_checksum((u8 *)req, RTPSE_MCU_MSG_SIZE - 1);
+
+ ret = pse->transport->send(pse, req);
+ if (ret)
+ return ret;
+
+ /*
+ * The MCU needs a fixed amount of time between receiving a request
+ * and having the response ready, regardless of how the bytes get to
+ * us. Pace the transaction here so each transport can keep its recv
+ * path simple: a single bounded wait rather than a generic retry.
+ */
+ msleep(RTPSE_MCU_RESPONSE_MS);
+
+ memset(resp, 0, sizeof(*resp));
+ ret = pse->transport->recv(pse, req, resp);
+ if (ret)
+ return ret;
+ }
+
+ /*
+ * Explicit MCU error opcodes (observed on the Gen1 dialect; harmless
+ * to check for Gen2 too). Catch these before the generic opcode/CRC
+ * mismatch path so callers see a meaningful errno.
+ */
+ switch (resp->opcode) {
+ case RTPSE_MCU_OPCODE_INCOMPLETE:
+ return -EBADE;
+ case RTPSE_MCU_OPCODE_BAD_CSUM:
+ return -EBADMSG;
+ case RTPSE_MCU_OPCODE_NOT_READY:
+ return -EAGAIN;
+ }
+
+ if (resp->opcode != req->opcode ||
+ resp->seq_num != req->seq_num ||
+ resp->checksum != rtpse_mcu_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))
+ return -EBADMSG;
+
+ return 0;
+}
+
+static int rtpse_mcu_port_query(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode,
+ struct rtpse_mcu_msg *resp)
+{
+ struct rtpse_mcu_msg req;
+ int ret;
+
+ rtpse_mcu_msg_init(&req, opcode);
+ req.payload[0] = port;
+
+ ret = rtpse_mcu_do_xfer(pse, &req, resp);
+ if (ret)
+ return ret;
+
+ if (resp->payload[0] != port)
+ return -EIO;
+
+ return 0;
+}
+
+static int rtpse_mcu_port_cmd(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode, u8 arg)
+{
+ struct rtpse_mcu_msg req, resp;
+ int ret;
+
+ rtpse_mcu_msg_init(&req, opcode);
+ req.payload[0] = port;
+ req.payload[1] = arg;
+
+ ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+ if (ret)
+ return ret;
+
+ if (resp.payload[0] != port || resp.payload[1] != 0)
+ return -EIO;
+
+ return 0;
+}
+
+/* Global operations */
+
+static int rtpse_mcu_get_info(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
+{
+ struct rtpse_mcu_msg req, resp;
+ const struct rtpse_mcu_opcode *opc;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_SYSTEM_INFO];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ rtpse_mcu_msg_init(&req, opc->op);
+ ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+ if (ret)
+ return ret;
+
+ pse->dialect->parse_system_info(resp.payload, info);
+ return 0;
+}
+
+static int rtpse_mcu_get_ext_config(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_ext_config *config)
+{
+ struct rtpse_mcu_msg req, resp;
+ const struct rtpse_mcu_opcode *opc;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_EXT_CONFIG];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ rtpse_mcu_msg_init(&req, opc->op);
+ ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+ if (ret)
+ return ret;
+
+ config->uvlo = resp.payload[0];
+ config->ovlo = resp.payload[5];
+ config->prealloc_enable = (resp.payload[1] == 0x1);
+ config->num_of_pses = resp.payload[6];
+
+ return 0;
+}
+
+static int rtpse_mcu_set_global_state(struct rtpse_mcu_ctrl *pse, bool enable)
+{
+ struct rtpse_mcu_msg req, resp;
+ const struct rtpse_mcu_opcode *opc;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_SET_GLOBAL_STATE];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ rtpse_mcu_msg_init(&req, opc->op);
+ req.payload[0] = enable ? 0x1 : 0x0;
+
+ ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+ if (ret)
+ return ret;
+
+ return (resp.payload[0] == 0x0) ? 0 : -EIO;
+}
+
+/* Port operations */
+
+static int rtpse_mcu_port_get_status(struct rtpse_mcu_ctrl *pse, unsigned int port,
+ struct rtpse_mcu_port_status *status)
+{
+ const struct rtpse_mcu_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_STATUS];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+ if (ret)
+ return ret;
+
+ status->sts1 = resp.payload[1];
+ status->sts2 = resp.payload[2];
+ status->sts3 = resp.payload[3];
+
+ return 0;
+}
+
+static int rtpse_mcu_port_get_measurement(struct rtpse_mcu_ctrl *pse, unsigned int port,
+ struct rtpse_mcu_port_measurement *measurement)
+{
+ const struct rtpse_mcu_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_POWER_STATS];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+ if (ret)
+ return ret;
+
+ measurement->voltage_raw = get_unaligned_be16(&resp.payload[1]);
+ measurement->current_raw = get_unaligned_be16(&resp.payload[3]);
+ measurement->temperature_raw = get_unaligned_be16(&resp.payload[5]);
+ measurement->power_raw = get_unaligned_be16(&resp.payload[7]);
+
+ return 0;
+}
+
+static int rtpse_mcu_port_get_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
+ struct rtpse_mcu_port_config *config)
+{
+ const struct rtpse_mcu_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_CONFIG];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+ if (ret)
+ return ret;
+
+ config->enable = (resp.payload[1] == 1);
+
+ return 0;
+}
+
+static int rtpse_mcu_port_get_ext_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
+ struct rtpse_mcu_port_ext_config *config)
+{
+ const struct rtpse_mcu_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+ if (ret)
+ return ret;
+
+ config->max_power = resp.payload[3];
+ config->priority = resp.payload[4];
+
+ return 0;
+}
+
+static int rtpse_mcu_port_set_state(struct rtpse_mcu_ctrl *pse, unsigned int port, bool enable)
+{
+ const struct rtpse_mcu_opcode *opc;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_ENABLE];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ return rtpse_mcu_port_cmd(pse, port, opc->op, enable ? 0x1 : 0x0);
+}
+
+/* PSE controller ops */
+
+static int rtpse_mcu_port_get_admin_state(struct pse_controller_dev *pcdev, int id,
+ struct pse_admin_state *admin_state)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_config config;
+ int ret;
+
+ ret = rtpse_mcu_port_get_config(pse, id, &config);
+ if (ret)
+ return ret;
+
+ admin_state->c33_admin_state = config.enable ? ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED :
+ ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
+ return 0;
+}
+
+static int rtpse_mcu_port_get_pw_status(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_status *pw_status)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_status status;
+ int ret;
+
+ ret = rtpse_mcu_port_get_status(pse, id, &status);
+ if (ret)
+ return ret;
+
+ switch (status.sts1) {
+ case RTPSE_MCU_PORT_STS_DISABLED:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+ break;
+ case RTPSE_MCU_PORT_STS_SEARCHING:
+ case RTPSE_MCU_PORT_STS_REQUESTING:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
+ break;
+ case RTPSE_MCU_PORT_STS_DELIVERING:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+ break;
+ case RTPSE_MCU_PORT_STS_TEST:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_TEST;
+ break;
+ case RTPSE_MCU_PORT_STS_FAULT:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
+ break;
+ case RTPSE_MCU_PORT_STS_OTHER_FAULT:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT;
+ break;
+ default:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
+ break;
+ }
+
+ return 0;
+}
+
+static int rtpse_mcu_port_get_pw_class(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_status status;
+ int ret;
+
+ ret = rtpse_mcu_port_get_status(pse, id, &status);
+ if (ret)
+ return ret;
+
+ /*
+ * sts2 holds the class only in the operational states below. In a
+ * fault/test state (or a code reserved on the current dialect) it
+ * does not, so there is no class to report and we return 0. That is
+ * indistinguishable from a real class-0 PD; userspace disambiguates
+ * via the power status.
+ */
+ switch (status.sts1) {
+ case RTPSE_MCU_PORT_STS_DISABLED:
+ case RTPSE_MCU_PORT_STS_SEARCHING:
+ case RTPSE_MCU_PORT_STS_DELIVERING:
+ case RTPSE_MCU_PORT_STS_REQUESTING:
+ return pse->dialect->parse_port_class(&status);
+ default:
+ return 0;
+ }
+}
+
+static int rtpse_mcu_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_measurement measurement;
+ int ret;
+
+ ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
+ if (ret)
+ return ret;
+
+ /* 100mW per LSB */
+ return measurement.power_raw * 100U;
+}
+
+static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_measurement measurement;
+ int ret;
+ u32 uV;
+
+ ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
+ if (ret)
+ return ret;
+
+ /* 64.45mV per LSB */
+ uV = measurement.voltage_raw * 64450U;
+ return min_t(u32, uV, INT_MAX);
+}
+
+static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)
+{
+ return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);
+}
+
+static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)
+{
+ return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);
+}
+
+static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_ext_config config;
+ int ret;
+
+ ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
+ if (ret)
+ return ret;
+
+ return config.max_power * pse->chip->pw_read_lsb_mW;
+}
+
+static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
+{
+ const struct rtpse_mcu_opcode *type_opc, *val_opc;
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ const struct rtpse_mcu_chip_info *chip = pse->chip;
+ u8 prg_val;
+ int ret;
+
+ if (max_mW < 0 || max_mW > chip->max_mW_per_port)
+ return -ERANGE;
+
+ type_opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE];
+ val_opc = &pse->dialect->opcode[chip->pw_set_cmd];
+ /* pw_set_lsb_mW is the divisor below; reject a chip that lacks it. */
+ if (!type_opc->valid || !val_opc->valid || !chip->pw_set_lsb_mW)
+ return -EOPNOTSUPP;
+
+ /*
+ * Switch the port to user-defined limit mode first, then program the
+ * limit value. If the second cmd fails, the port is left in
+ * user-defined mode but with the previous limit value; the next
+ * successful set_pw_limit call recovers it.
+ */
+ ret = rtpse_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
+ if (ret)
+ return ret;
+
+ prg_val = min_t(unsigned int, max_mW / chip->pw_set_lsb_mW, U8_MAX);
+
+ return rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
+}
+
+static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_limit_ranges *out)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct ethtool_c33_pse_pw_limit_range *range;
+
+ range = kzalloc_obj(*range);
+ if (!range)
+ return -ENOMEM;
+
+ range[0].min = 0;
+ range[0].max = pse->chip->max_mW_per_port;
+
+ out->c33_pw_limit_ranges = range;
+ return 1;
+}
+
+static int rtpse_mcu_port_get_prio(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ struct rtpse_mcu_port_ext_config config;
+ int ret;
+
+ ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
+ if (ret)
+ return ret;
+
+ return config.priority;
+}
+
+static int rtpse_mcu_port_set_prio(struct pse_controller_dev *pcdev, int id, unsigned int prio)
+{
+ struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+ const struct rtpse_mcu_opcode *opc;
+
+ if (prio > RTPSE_MCU_PORT_MAX_PRIORITY)
+ return -ERANGE;
+
+ opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_PRIORITY];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ return rtpse_mcu_port_cmd(pse, id, opc->op, prio);
+}
+
+static const struct pse_controller_ops rtpse_mcu_ops = {
+ .pi_get_admin_state = rtpse_mcu_port_get_admin_state,
+ .pi_get_pw_status = rtpse_mcu_port_get_pw_status,
+ .pi_get_pw_class = rtpse_mcu_port_get_pw_class,
+ .pi_get_actual_pw = rtpse_mcu_port_get_actual_pw,
+ .pi_enable = rtpse_mcu_port_enable,
+ .pi_disable = rtpse_mcu_port_disable,
+ .pi_get_voltage = rtpse_mcu_port_get_voltage,
+ .pi_get_pw_limit = rtpse_mcu_port_get_pw_limit,
+ .pi_set_pw_limit = rtpse_mcu_port_set_pw_limit,
+ .pi_get_pw_limit_ranges = rtpse_mcu_port_get_pw_limit_ranges,
+ .pi_get_prio = rtpse_mcu_port_get_prio,
+ .pi_set_prio = rtpse_mcu_port_set_prio,
+};
+
+static int rtpse_mcu_discover(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
+{
+ struct rtpse_mcu_ext_config ext_config;
+ unsigned long deadline;
+ int ret;
+
+ /*
+ * A booting MCU may stay silent (-ETIMEDOUT), not ACK its address
+ * (-ENXIO / -EREMOTEIO), report not-ready (-EAGAIN), or emit a
+ * corrupt/partial frame (-EBADMSG / -EBADE). Retry those within a
+ * bounded window; other errors (e.g. -EOPNOTSUPP) are fatal and fail
+ * immediately.
+ */
+ deadline = jiffies + msecs_to_jiffies(RTPSE_MCU_BOOT_TIMEOUT_MS);
+ do {
+ ret = rtpse_mcu_get_info(pse, info);
+ if (ret != -ETIMEDOUT && ret != -ENXIO && ret != -EREMOTEIO &&
+ ret != -EAGAIN && ret != -EBADMSG && ret != -EBADE)
+ break;
+ msleep(RTPSE_MCU_BOOT_RETRY_MS);
+ } while (time_before(jiffies, deadline));
+ if (ret)
+ return dev_err_probe(pse->dev, ret, "failed to read MCU info\n");
+
+ switch (info->device_id) {
+ case RTPSE_MCU_DEVICE_ID_RTL8238B:
+ pse->chip = &rtl8238b_info;
+ break;
+ case RTPSE_MCU_DEVICE_ID_RTL8239:
+ pse->chip = &rtl8239_info;
+ break;
+ case RTPSE_MCU_DEVICE_ID_RTL8239C:
+ pse->chip = &rtl8239c_info;
+ break;
+ case RTPSE_MCU_DEVICE_ID_BCM59111:
+ pse->chip = &bcm59111_info;
+ break;
+ case RTPSE_MCU_DEVICE_ID_BCM59121:
+ pse->chip = &bcm59121_info;
+ break;
+ default:
+ return dev_err_probe(pse->dev, -EINVAL, "unknown PSE id 0x%x\n",
+ info->device_id);
+ }
+
+ if (!info->max_ports || info->max_ports > RTPSE_MCU_MAX_PORTS)
+ return dev_err_probe(pse->dev, -EINVAL,
+ "MCU reports invalid port count %u\n", info->max_ports);
+
+ ret = rtpse_mcu_get_ext_config(pse, &ext_config);
+ if (ret)
+ return dev_err_probe(pse->dev, ret, "failed to read MCU ext config\n");
+
+ dev_info(pse->dev, "%s MCU, %s (id 0x%04x), %u ports across %u PSE chip(s)\n",
+ pse->dialect->mcu_type_str(info->mcu_type), pse->chip->name,
+ info->device_id, info->max_ports, ext_config.num_of_pses);
+ return 0;
+}
+
+static void rtpse_mcu_regulator_disable(void *data)
+{
+ regulator_disable(data);
+}
+
+static void rtpse_mcu_global_disable(void *data)
+{
+ struct rtpse_mcu_ctrl *pse = data;
+
+ rtpse_mcu_set_global_state(pse, false);
+}
+
+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse)
+{
+ const struct rtpse_mcu_match_data *match;
+ struct gpio_desc *enable_gpio;
+ struct rtpse_mcu_info info;
+ int ret;
+
+ BUILD_BUG_ON(sizeof(struct rtpse_mcu_msg) != RTPSE_MCU_MSG_SIZE);
+
+ ret = devm_mutex_init(pse->dev, &pse->mutex);
+ if (ret)
+ return ret;
+
+ match = device_get_match_data(pse->dev);
+ if (!match)
+ return dev_err_probe(pse->dev, -ENODEV, "missing match data\n");
+ pse->dialect = match->dialect;
+
+ /*
+ * Catch a dialect that forgot to set one of the required hooks at
+ * probe time, rather than NULL-deref'ing later from a fast path.
+ */
+ if (!pse->dialect ||
+ !pse->dialect->parse_system_info ||
+ !pse->dialect->parse_port_class ||
+ !pse->dialect->mcu_type_str)
+ return dev_err_probe(pse->dev, -EINVAL,
+ "dialect for chip is incomplete\n");
+
+ pse->poe_supply = devm_regulator_get(pse->dev, "power");
+ if (IS_ERR(pse->poe_supply))
+ return dev_err_probe(pse->dev, PTR_ERR(pse->poe_supply),
+ "failed to get PoE supply\n");
+
+ enable_gpio = devm_gpiod_get_optional(pse->dev, "enable", GPIOD_OUT_HIGH);
+ if (IS_ERR(enable_gpio))
+ return dev_err_probe(pse->dev, PTR_ERR(enable_gpio),
+ "failed to get enable gpio\n");
+
+ ret = rtpse_mcu_discover(pse, &info);
+ if (ret)
+ return ret;
+
+ ret = regulator_enable(pse->poe_supply);
+ if (ret)
+ return dev_err_probe(pse->dev, ret, "failed to enable PoE supply\n");
+
+ ret = devm_add_action_or_reset(pse->dev, rtpse_mcu_regulator_disable, pse->poe_supply);
+ if (ret)
+ return ret;
+
+ if (!info.system_enable) {
+ ret = rtpse_mcu_set_global_state(pse, true);
+ /* Dialects without a global-state concept (e.g. Gen1) return
+ * -EOPNOTSUPP; treat that as "no separate enable required".
+ */
+ if (ret && ret != -EOPNOTSUPP)
+ return dev_err_probe(pse->dev, ret,
+ "failed to enable PSE system\n");
+ if (!ret) {
+ ret = devm_add_action_or_reset(pse->dev,
+ rtpse_mcu_global_disable, pse);
+ if (ret)
+ return ret;
+ }
+ }
+
+ /*
+ * Depending on the MCU firmware configuration (which might be different
+ * for every board), it isn't known whether the PoE subsystem is active or
+ * inactive by default. At this stage, the PSE chips might already deliver
+ * power to PDs without any explicit enable.
+ */
+
+ /* pcdev.owner is set by the transport, so the registered controller
+ * pins the transport module that owns the live device, not the core.
+ */
+ pse->pcdev.ops = &rtpse_mcu_ops;
+ pse->pcdev.dev = pse->dev;
+ pse->pcdev.types = ETHTOOL_PSE_C33;
+ pse->pcdev.nr_lines = info.max_ports;
+ pse->pcdev.pis_prio_max = RTPSE_MCU_PORT_MAX_PRIORITY;
+ pse->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;
+
+ return devm_pse_controller_register(pse->dev, &pse->pcdev);
+}
+EXPORT_SYMBOL_GPL(rtpse_mcu_register);
+
+static void rtpse_mcu_gen2_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
+{
+ info->max_ports = payload[1];
+ info->system_enable = (payload[2] == 0x1);
+ info->device_id = get_unaligned_be16(&payload[3]);
+ info->sw_ver = payload[5];
+ info->mcu_type = payload[6];
+ info->config_status = payload[7];
+ info->ext_ver = payload[8];
+}
+
+static int rtpse_mcu_gen2_parse_port_class(const struct rtpse_mcu_port_status *status)
+{
+ /* Class lives in the upper nibble of sts2. */
+ return FIELD_GET(GENMASK(7, 4), status->sts2);
+}
+
+static const char *rtpse_mcu_gen2_mcu_type_str(unsigned int mcu_type)
+{
+ switch (mcu_type) {
+ case 0x00: return "GigaDevice GD32F310";
+ case 0x01: return "GigaDevice GD32F230";
+ case 0x02: return "GigaDevice GD32F303";
+ case 0x03: return "GigaDevice GD32F103";
+ case 0x04: return "GigaDevice GD32E103";
+ case 0x10: return "Nuvoton M0516";
+ case 0x11: return "Nuvoton M0564";
+ case 0x12: return "Nuvoton NUC029";
+ default: return "unknown";
+ }
+}
+
+static void rtpse_mcu_gen1_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
+{
+ info->max_ports = payload[1];
+ /* Gen1 has no explicit system_enable byte; the closest analog is the
+ * "remote enable" bit in the system-status flags at payload[7].
+ */
+ info->system_enable = !!(payload[7] & BIT(2));
+ info->device_id = get_unaligned_be16(&payload[3]);
+ info->sw_ver = payload[5];
+ info->mcu_type = payload[6];
+ info->config_status = payload[7];
+ info->ext_ver = payload[8];
+}
+
+static int rtpse_mcu_gen1_parse_port_class(const struct rtpse_mcu_port_status *status)
+{
+ /* Gen1 puts the detected class in payload[3] (== sts3) directly.
+ * Mask to the low nibble; class is 0..8 and any high bits would be
+ * noise.
+ */
+ return status->sts3 & 0x0f;
+}
+
+static const char *rtpse_mcu_gen1_mcu_type_str(unsigned int mcu_type)
+{
+ switch (mcu_type) {
+ case 0x00: return "ST Micro ST32F100";
+ case 0x01: return "Nuvoton M05xx LAN";
+ case 0x02: return "ST Micro STF030C8";
+ case 0x03: return "Nuvoton M058SAN";
+ case 0x04: return "Nuvoton NUC122";
+ default: return "unknown";
+ }
+}
+
+/* Map each logical command the core issues to its per-dialect opcode. */
+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen2 = {
+ .parse_system_info = rtpse_mcu_gen2_parse_system_info,
+ .parse_port_class = rtpse_mcu_gen2_parse_port_class,
+ .mcu_type_str = rtpse_mcu_gen2_mcu_type_str,
+ .opcode = {
+ [RTPSE_MCU_CMD_SET_GLOBAL_STATE] = RTPSE_MCU_OP(0x00),
+ [RTPSE_MCU_CMD_GET_SYSTEM_INFO] = RTPSE_MCU_OP(0x40),
+ [RTPSE_MCU_CMD_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x4a),
+
+ [RTPSE_MCU_CMD_PORT_ENABLE] = RTPSE_MCU_OP(0x01),
+ [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x12),
+ [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT] = RTPSE_MCU_OP(0x13),
+ [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_MCU_OP(0x14),
+ [RTPSE_MCU_CMD_PORT_SET_PRIORITY] = RTPSE_MCU_OP(0x15),
+ [RTPSE_MCU_CMD_PORT_GET_STATUS] = RTPSE_MCU_OP(0x42),
+ [RTPSE_MCU_CMD_PORT_GET_POWER_STATS] = RTPSE_MCU_OP(0x44),
+ [RTPSE_MCU_CMD_PORT_GET_CONFIG] = RTPSE_MCU_OP(0x48),
+ [RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x49),
+ },
+};
+
+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen1 = {
+ .parse_system_info = rtpse_mcu_gen1_parse_system_info,
+ .parse_port_class = rtpse_mcu_gen1_parse_port_class,
+ .mcu_type_str = rtpse_mcu_gen1_mcu_type_str,
+ .opcode = {
+ [RTPSE_MCU_CMD_GET_SYSTEM_INFO] = RTPSE_MCU_OP(0x20),
+ [RTPSE_MCU_CMD_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x2b),
+
+ [RTPSE_MCU_CMD_PORT_ENABLE] = RTPSE_MCU_OP(0x00),
+ [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x15),
+ [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT] = RTPSE_MCU_OP(0x16),
+ [RTPSE_MCU_CMD_PORT_SET_PRIORITY] = RTPSE_MCU_OP(0x1a),
+ [RTPSE_MCU_CMD_PORT_GET_STATUS] = RTPSE_MCU_OP(0x21),
+ [RTPSE_MCU_CMD_PORT_GET_POWER_STATS] = RTPSE_MCU_OP(0x30),
+ [RTPSE_MCU_CMD_PORT_GET_CONFIG] = RTPSE_MCU_OP(0x25),
+ [RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x26),
+ },
+};
+
+const struct rtpse_mcu_match_data rtpse_mcu_gen1_data = {
+ .dialect = &rtpse_mcu_dialect_gen1,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_gen1_data);
+
+const struct rtpse_mcu_match_data rtpse_mcu_gen2_data = {
+ .dialect = &rtpse_mcu_dialect_gen2,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_data);
+
+/* Same dialect as gen2, but the MCU expects raw-I2C framing. */
+const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data = {
+ .dialect = &rtpse_mcu_dialect_gen2,
+ .native_i2c = true,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_i2c_data);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek PSE MCU driver (core)");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse-mcu.h b/drivers/net/pse-pd/realtek-pse-mcu.h
new file mode 100644
index 000000000000..7b2257355299
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef _REALTEK_PSE_MCU_H
+#define _REALTEK_PSE_MCU_H
+
+#include <linux/mutex.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/types.h>
+
+/*
+ * Time the MCU itself needs between accepting a request and having a
+ * response ready. These are properties of the MCU firmware, not of the
+ * underlying transport: the core paces transactions by RTPSE_MCU_RESPONSE_MS
+ * and both transports size their per-transaction recv ceiling from
+ * RTPSE_MCU_RESPONSE_MAX_MS, since some commands are documented as
+ * needing up to ~1s to produce a reply.
+ */
+#define RTPSE_MCU_RESPONSE_MS 25
+#define RTPSE_MCU_RESPONSE_MAX_MS 1000
+
+/*
+ * Total time to keep retrying the first MCU read at probe, and the pause
+ * between attempts. Right after enable-gpios is asserted the MCU may not
+ * answer on the bus yet; give it a bounded window to come up before
+ * declaring the probe failed.
+ */
+#define RTPSE_MCU_BOOT_TIMEOUT_MS 3000
+#define RTPSE_MCU_BOOT_RETRY_MS 100
+
+#define RTPSE_MCU_MSG_SIZE 12
+
+struct rtpse_mcu_msg {
+ u8 opcode;
+ u8 seq_num;
+ u8 payload[9];
+ u8 checksum;
+} __packed;
+
+/*
+ * MCU status opcodes (seen on the Gen1 dialect; Gen2 never emits them).
+ * INCOMPLETE/BAD_CSUM are terminal; NOT_READY is transient.
+ */
+#define RTPSE_MCU_OPCODE_INCOMPLETE 0xfd /* -EBADE */
+#define RTPSE_MCU_OPCODE_BAD_CSUM 0xfe /* -EBADMSG */
+#define RTPSE_MCU_OPCODE_NOT_READY 0xff /* -EAGAIN */
+
+/* A polling transport can stop here: the matching reply, or a terminal error. */
+static inline bool rtpse_mcu_resp_is_final(const struct rtpse_mcu_msg *req,
+ const struct rtpse_mcu_msg *resp)
+{
+ return resp->opcode == req->opcode ||
+ resp->opcode == RTPSE_MCU_OPCODE_INCOMPLETE ||
+ resp->opcode == RTPSE_MCU_OPCODE_BAD_CSUM;
+}
+
+/* Opaque to transports; defined in realtek-pse-core.c. */
+struct rtpse_mcu_dialect;
+struct rtpse_mcu_chip_info;
+struct rtpse_mcu_ctrl;
+
+/* Per-compatible match data (the of_match .data). */
+struct rtpse_mcu_match_data {
+ const struct rtpse_mcu_dialect *dialect;
+ bool native_i2c; /* raw-I2C framing (vs SMBus); I2C transport only */
+};
+
+struct rtpse_mcu_transport_ops {
+ int (*send)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req);
+ int (*recv)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp);
+};
+
+struct rtpse_mcu_ctrl {
+ struct device *dev;
+ struct pse_controller_dev pcdev;
+ struct mutex mutex; /* serializes MCU request/response transactions */
+ const struct rtpse_mcu_dialect *dialect;
+ const struct rtpse_mcu_chip_info *chip;
+ const struct rtpse_mcu_transport_ops *transport;
+ u8 seq; /* rolling request seq_num, echoed by the MCU */
+
+ struct regulator *poe_supply;
+};
+
+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse);
+
+extern const struct rtpse_mcu_match_data rtpse_mcu_gen1_data;
+extern const struct rtpse_mcu_match_data rtpse_mcu_gen2_data;
+extern const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data;
+
+#endif
--
2.53.0
^ permalink raw reply related
* [PATCH net-next v7 1/4] dt-bindings: net: pse-pd: add bindings for Realtek PSE MCU
From: Jonas Jelonek @ 2026-07-12 19:22 UTC (permalink / raw)
To: Oleksij Rempel, Kory Maincent, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
Jonas Jelonek, Conor Dooley
In-Reply-To: <20260712192251.1413279-1-jelonek.jonas@gmail.com>
Add a binding for the microcontroller (MCU) that fronts the PSE silicon
on a range of managed Realtek-based switches. The host talks only to the
MCU, over I2C/SMBus or UART, using a fixed message-based protocol; the
PSE chips behind it never appear on the bus.
The device is the MCU together with its Realtek firmware: the firmware
and its host protocol are what the binding describes, not the
general-purpose microcontroller they run on. The PSE silicon behind the
MCU (Realtek or Broadcom) is reported by the MCU and detected at runtime,
so it is not described here - hence the 'realtek' vendor prefix.
Two protocol generations exist, both Realtek's, selected by the
compatible: gen1 on older boards (fronting Broadcom PSE silicon) and gen2,
the altered protocol used with Realtek's own PSE silicon. On an I2C
attachment the framing the MCU firmware expects is part of the compatible
as well - '-smbus' or raw '-i2c'; a UART attachment carries no framing
suffix, as the transport is given by the parent serial node.
Each board additionally carries a device-specific compatible that falls
back to one of the protocol compatibles above. Drivers bind on the
protocol compatible; the device-specific string identifies the board and
reserves a place for a future per-board quirk without having to retrofit
device trees already in the field.
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
---
.../net/pse-pd/realtek,pse-mcu-gen1.yaml | 176 ++++++++++++++++++
1 file changed, 176 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
diff --git a/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
new file mode 100644
index 000000000000..2b0661c9e31f
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
@@ -0,0 +1,176 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/pse-pd/realtek,pse-mcu-gen1.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Realtek PSE MCU
+
+maintainers:
+ - Jonas Jelonek <jelonek.jonas@gmail.com>
+
+description: |
+ A microcontroller (MCU) that manages the PSE (Power Sourcing Equipment)
+ hardware on a range of managed PoE switches. The host CPU talks only to
+ this MCU - over I2C/SMBus or UART - using a small message-based protocol;
+ the PSE silicon it drives sits behind the MCU and is never accessed
+ directly. For example, on the Zyxel GS1900-10HP the SoC reaches the MCU
+ over UART, and the MCU manages the on-board PSE chip.
+
+ This binding describes the MCU together with its Realtek firmware: the
+ firmware and its host protocol, which are stable across boards. The
+ microcontroller silicon is a general-purpose part that varies, and the
+ PSE silicon behind the MCU (Realtek RTL823x/RTL8239* or Broadcom
+ BCM59xxx) is reported by the MCU and detected at runtime - neither is
+ named here.
+
+ Two protocol generations exist, both Realtek's:
+ gen1 older boards, where the MCU fronts Broadcom PSE silicon
+ gen2 the altered protocol used with Realtek's own PSE silicon
+
+ On an I2C attachment the framing the MCU firmware expects is part of the
+ compatible: '-smbus' (reads carry a leading command byte and a repeated
+ start) or '-i2c' (bare block writes and reads). A UART attachment carries
+ no framing suffix; the transport is given by the parent 'serial' node.
+
+ Each board additionally carries a device-specific compatible that falls
+ back to one of the protocol compatibles above. Drivers bind on the
+ protocol compatible; the device-specific string identifies the board and
+ reserves a place for a future per-board quirk without having to retrofit
+ device trees already in the field.
+
+properties:
+ compatible:
+ oneOf:
+ # UART
+ - items:
+ - enum:
+ - zyxel,gs1900-10hp-a1-pse
+ - const: realtek,pse-mcu-gen1
+
+ # I2C, SMBus framing
+ - items:
+ - enum:
+ - zyxel,gs1920-24hp-v2-pse
+ - const: realtek,pse-mcu-gen1-smbus
+
+ # UART
+ - items:
+ - enum:
+ - zyxel,gs1900-10hp-b1-pse
+ - zyxel,xmg1915-10ep-pse
+ - const: realtek,pse-mcu-gen2
+
+ # I2C, SMBus framing
+ - items:
+ - enum:
+ - zyxel,xs1930-12hp-pse
+ - const: realtek,pse-mcu-gen2-smbus
+
+ # I2C, raw framing
+ - items:
+ - enum:
+ - linksys,lgs328mpc-v2-pse
+ - const: realtek,pse-mcu-gen2-i2c
+
+ reg:
+ maxItems: 1
+
+ power-supply:
+ description: Regulator supplying the PoE power rail.
+
+ enable-gpios:
+ maxItems: 1
+
+required:
+ - compatible
+
+allOf:
+ - $ref: pse-controller.yaml#
+ - $ref: /schemas/serial/serial-peripheral-props.yaml#
+ # A '-smbus'/'-i2c' compatible is an I2C attachment: it has 'reg' and
+ # cannot carry serial bus properties. A bare gen compatible is a UART
+ # attachment: no 'reg', the transport comes from the parent serial node.
+ - if:
+ properties:
+ compatible:
+ contains:
+ enum:
+ - realtek,pse-mcu-gen1-smbus
+ - realtek,pse-mcu-gen2-smbus
+ - realtek,pse-mcu-gen2-i2c
+ then:
+ required:
+ - reg
+ properties:
+ current-speed: false
+ max-speed: false
+ else:
+ properties:
+ reg: false
+
+unevaluatedProperties: false
+
+examples:
+ # SMBus-framed I2C attachment
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-pse@20 {
+ compatible = "zyxel,xs1930-12hp-pse", "realtek,pse-mcu-gen2-smbus";
+ reg = <0x20>;
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ };
+ };
+ };
+ };
+
+ # Raw-I2C-framed attachment
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-pse@20 {
+ compatible = "linksys,lgs328mpc-v2-pse", "realtek,pse-mcu-gen2-i2c";
+ reg = <0x20>;
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ };
+ };
+ };
+ };
+
+ # UART attachment
+ - |
+ serial {
+ ethernet-pse {
+ compatible = "zyxel,gs1900-10hp-a1-pse", "realtek,pse-mcu-gen1";
+ current-speed = <19200>;
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ };
+ };
+ };
+ };
--
2.53.0
^ permalink raw reply related
* [PATCH net-next v7 0/4] net: pse-pd: add Realtek PSE MCU support
From: Jonas Jelonek @ 2026-07-12 19:22 UTC (permalink / raw)
To: Oleksij Rempel, Kory Maincent, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
Jonas Jelonek
This series adds a PSE-PD driver for the microcontroller (MCU) that
fronts the PSE silicon on a range of managed switches, together with its
DT binding.
Hardware model
==============
These boards do not expose the PSE chips to the host directly. A small
microcontroller sits on an I2C/SMBus or UART bus and manages one or more
PSE chips behind it; the host CPU only ever talks to that MCU, using a
fixed 12-byte request/response protocol with a trailing checksum. The
PSE silicon never appears on the bus.
Two generations of the protocol exist, both Realtek's: an older one on
boards with Broadcom PSE silicon (BCM59111, BCM59121) and a newer one
used with Realtek's own PSE silicon (RTL8238B, RTL8239, RTL8239C). They
diverge in opcode numbering and a few response layouts; the driver
abstracts that behind a per-dialect opcode table and parser hooks,
selected by the compatible. The specific PSE chip behind the MCU is
detected at runtime and only influences per-chip constants (power scaling
and the per-port cap).
The compatibles
===============
The protocol compatibles name two generations of the Realtek protocol,
with the I2C framing folded in:
realtek,pse-mcu-gen1 gen1, UART
realtek,pse-mcu-gen1-smbus gen1, I2C/SMBus
realtek,pse-mcu-gen2 gen2, UART
realtek,pse-mcu-gen2-smbus gen2, I2C/SMBus
realtek,pse-mcu-gen2-i2c gen2, raw I2C
and each board carries a device-specific compatible that falls back to one
of these, e.g.
compatible = "zyxel,xs1930-12hp-pse", "realtek,pse-mcu-gen2-smbus";
The naming is the part most likely to raise questions, so the reasoning up
front (the binding documents it too):
- The node describes the MCU together with its Realtek firmware, not a
PSE chip and not the microcontroller silicon. The PSE chips sit behind
the MCU, never appear on the bus, and are reported by the MCU and
detected at runtime; the microcontroller itself is a general-purpose
part (GigaDevice, Nuvoton, ...) that varies across boards. What is
fixed and Realtek's is the firmware and its host protocol - hence the
'realtek' prefix.
- gen1 and gen2 are two generations of that protocol, both Realtek's:
gen1 on older boards fronting Broadcom PSE silicon, gen2 the altered
protocol used once Realtek shipped their own PSE silicon. The
generation is fixed per board and is all the driver needs at DT-parse
time, so the compatible encodes it.
- On I2C the MCU firmware expects one of two framings - SMBus or raw
I2C - which is a genuine programming-model difference, so it is part
of the compatible ('-smbus' / '-i2c'). A UART attachment carries no
framing suffix; the transport is given structurally by the parent
'serial' node.
- Each board additionally carries a device-specific compatible that
falls back to the protocol one. The driver only ever binds on the
protocol compatible; the device-specific string keeps the binding
specific and reserves a place for a future per-board quirk without
having to retrofit device trees already deployed in the field.
Testing
=======
- Linksys LGS328MPCv2 (RTL8238B, I2C)
- Zyxel GS1900-10HP A1 (BCM59121, UART)
- Zyxel GS1900-10HP B1 (RTL8238B, UART)
- Zyxel GS1920-24HPv2 (BCM59121, SMBus)
- Zyxel XMG1915-10EP (RTL8239C, UART)
- Zyxel XS1930-12HP (RTL8239, SMBus)
---
v6 -> v7:
- dt-bindings: rename file to 'realtek,pse-mcu-gen1.yml', using
specific compatible as file name (Conor)
- dt-bindings: added Conor's Reviewed-by
- core: replaced some leftover 'BCM/RTK' dialect framing in comments
- core: use rolling sequence number instead of always hardcoded 0xff,
adressing the issue that stale data might be accepted for new
requests (Sashiko)
- core: reordered MODULE_ calls to keep consistent across all driver
parts
- i2c: replaced some leftover 'BCM/RTK' dialect framing in comments
- slight commit message adjustments
v6: https://lore.kernel.org/netdev/20260709194125.2784507-1-jelonek.jonas@gmail.com/
v5 -> v6:
- dt-bindings: reworked the compatibles per DT-maintainer feedback
- name the two protocol generations -gen1 / -gen2 (both Realtek's)
instead of the -rtk / -brcm dialect suffix (Conor)
- encode the I2C framing in the compatible (-smbus / raw -i2c) and
drop the realtek,i2c-protocol property (Rob)
- add device-specific (switch) compatibles that fall back to the
protocol compatibles, with the board↔protocol pairing enforced in
the schema (Conor)
- rewrite the description accordingly
- driver: track the binding rework
- match on realtek,pse-mcu-gen{1,2}[-smbus|-i2c]; the I2C transport
selects SMBus-vs-raw framing from a native_i2c match-data flag
instead of reading the property (drops rtpse_mcu_needs_i2c_proto)
- rename the internal dialect and parser symbols rtk/brcm → gen1/gen2
(chip identifiers like RTL8238B/BCM59121 kept)
- i2c: DMA-safe raw-I2C path — bounce each frame through a heap buffer,
since i2c_master_send()/i2c_master_recv() may DMA and the core's
frame buffers are on the stack (SMBus and UART paths unaffected)
(sashiko-nipa)
- includes: drop unused linux/mod_devicetable.h (core) (Uwe)
- includes: drop unused linux/delay.h (uart); add
linux/regulator/consumer.h (core) and linux/slab.h + linux/string.h
(i2c)
- commit messages — update the binding, core, and I2C messages to
match (generations, framing-in-compatible, DMA note)
v5: https://lore.kernel.org/netdev/20260706112425.3149226-1-jelonek.jonas@gmail.com/
v4 -> v5:
- split the single driver patch into three — core / I2C transport / UART
transport. Binding stays patch 1, unchanged in shape. (Paolo)
Please give guidance on how to if I should split more.
- core: set_pw_limit: guard divide-by-zero on pw_set_lsb_mW; cap the
programmed value with U8_MAX instead of a bare 0xff; prg_val is now u8.
(Oleksij, Sashiko)
- core: discover: also retry transient boot-time frames (-EBADMSG / -EBADE)
within the bounded window, not just silence/NAK/not-ready (Sashiko).
- core: pw_status: report Broadcom 0x3 → TEST and 0x5 → OTHERFAULT
(new STS_TEST/STS_OTHER_FAULT); pw_class comment corrected (0x3/0x5
aren't "other fault" on RTL; class-0-vs-fault note). (Sashiko)
- core: dropped unused decoded fields — function_mode, cls_type,
disconnect_type, pair_type, inrush_mode, limit_type, chip_addr,
channel. (Oleksij)
- core: removed forward declarations by moving the response structs above
the dialect struct. (Oleksij)
- core: get_pw_limit_ranges: reverse-Christmas-tree local ordering.
(Oleksij)
- core: dialect comment clarified (only divergent responses are hooked);
commit message "parser hooks" tightened to "…for the responses that
differ." (Sashiko)
- core: made parse_system_info hook void, both implementations return
hardcoded 0. (Paolo)
- core: dropped GFP_KERNEL from kzalloc_obj. (Paolo)
- core: dropped unneeded u32 cast
- kept probe dev_info() for now deliberately, due to different opinions
on whether a probe might print or not
- NOT included Acked-by from Oleksij, due to several changes
v4: https://lore.kernel.org/netdev/20260630105651.756058-1-jelonek.jonas@gmail.com/
v3 -> v4:
- move owner setting from core to transport, mitigating possible
use-after-free (Sashiko)
- resend because net-next was still closed
v3: https://lore.kernel.org/netdev/20260628222705.4052815-1-jelonek.jonas@gmail.com/
v2 -> v3:
- dt-bindings: using brcm instead of bcm for Broadcom
- rename the driver files and Kconfig symbols to realtek-pse-mcu-* /
PSE_REALTEK_MCU* for consistency with the realtek,pse-mcu-* compatibles
- rename driver-internal prefix from 'rtpse_' to 'rtpse_mcu' to
emphasize this targets the MCU-centric setup (and leaves room open
for eventual directly addressable PSE chips)
- rework the vendor-prefix rationale (binding + commit message): the
prefix names the protocol/firmware owner (Realtek documents the protocol
and supplies the firmware), and -rtk/-brcm select the Realtek or Broadcom
protocol dialect
- core: reject zeroed/echo-mismatched responses via the echoed seq_num
(a BCM PORT_ENABLE on port 0 was otherwise accepted from an all-zero
frame)
- core: enable the PoE supply before global-enabling the MCU, and roll
back the global enable on probe failure or driver removal
- core: drop inline from helpers (flagged by automated check)
- uart: update the completion under rx_lock too, so a late frame can no
longer make the next transaction fail spuriously with -EIO
v2: https://lore.kernel.org/netdev/20260612132944.460646-1-jelonek.jonas@gmail.com/
v1 -> v2:
- all points flagged by Sashiko addressed:
- uart: drop frame overflow (return count, not the stored length) so
serdev retains no leftover bytes that would misalign the next response
- uart: guard rx_buf/rx_len with a spinlock to close a data race between
the async receive_buf callback and send/recv
- i2c: return terminal MCU error opcodes (0xfd/0xfe) to the core
immediately instead of polling to the 1 s timeout
- core: cap BCM59121 at 30 W (802.3at) — the basic 8-bit set command
can't program the advertised 60 W (it silently clamped to 51 W)
v1: https://lore.kernel.org/netdev/20260608205758.1830521-1-jelonek.jonas@gmail.com/
---
Jonas Jelonek (4):
dt-bindings: net: pse-pd: add bindings for Realtek PSE MCU
net: pse-pd: add Realtek PSE MCU core
net: pse-pd: realtek-pse-mcu: add I2C transport
net: pse-pd: realtek-pse-mcu: add UART transport
.../net/pse-pd/realtek,pse-mcu-gen1.yaml | 176 ++++
MAINTAINERS | 7 +
drivers/net/pse-pd/Kconfig | 28 +
drivers/net/pse-pd/Makefile | 3 +
drivers/net/pse-pd/realtek-pse-mcu-core.c | 995 ++++++++++++++++++
drivers/net/pse-pd/realtek-pse-mcu-i2c.c | 170 +++
drivers/net/pse-pd/realtek-pse-mcu-uart.c | 155 +++
drivers/net/pse-pd/realtek-pse-mcu.h | 91 ++
8 files changed, 1625 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-core.c
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-i2c.c
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-uart.c
create mode 100644 drivers/net/pse-pd/realtek-pse-mcu.h
base-commit: f6f3b36c15ed44de1fbb44e645e4fae8c4a4453e
--
2.53.0
^ permalink raw reply
* [PATCH net-next 3/3] selftests: net: add coverage for fdb nexthop dst port
From: Jack Ma @ 2026-07-12 19:12 UTC (permalink / raw)
To: netdev, David Ahern
Cc: Ido Schimmel, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Shuah Khan, linux-kselftest,
linux-kernel, Jack Ma
In-Reply-To: <20260712191218.236-1-jack4it@gmail.com>
Add fib_nexthops_fdb_port.sh, which exercises the NHA_FDB_PORT rules:
accept a port on an fdb nexthop that has a gateway and echo it back on
dump, reject it on non-fdb or gateway-less nexthops, allow a group whose
legs differ only in UDP port, and confirm a portless fdb nexthop omits
the attribute. The test SKIPs cleanly on kernels or iproute2 without
NHA_FDB_PORT support.
Signed-off-by: Jack Ma <jack4it@gmail.com>
---
tools/testing/selftests/net/Makefile | 1 +
.../selftests/net/fib_nexthops_fdb_port.sh | 78 +++++++++++++++++++
2 files changed, 79 insertions(+)
create mode 100755 tools/testing/selftests/net/fib_nexthops_fdb_port.sh
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 708d960ae..c06eb4927 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -36,6 +36,7 @@ TEST_PROGS := \
fib_nexthop_multiprefix.sh \
fib_nexthop_nongw.sh \
fib_nexthops.sh \
+ fib_nexthops_fdb_port.sh \
fib_rule_tests.sh \
fib_tests.sh \
fin_ack_lat.sh \
diff --git a/tools/testing/selftests/net/fib_nexthops_fdb_port.sh b/tools/testing/selftests/net/fib_nexthops_fdb_port.sh
new file mode 100755
index 000000000..8b401c6d2
--- /dev/null
+++ b/tools/testing/selftests/net/fib_nexthops_fdb_port.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Control-plane selftest for per-nexthop VXLAN fdb destination port
+# (NHA_FDB_PORT). Verifies the accept/reject rules and the dump roundtrip.
+# No datapath traffic here -- see tests/integ/vxlan-fdb-port-integ.sh for the
+# real forwarding test.
+#
+# Requires: patched kernel (NHA_FDB_PORT) and patched iproute2 (the "port"
+# keyword on "ip nexthop ... fdb"). SKIPs cleanly otherwise.
+
+set -u
+
+ksft_skip=4
+NS="nhfdbport-$$"
+IP="ip -netns $NS"
+ret=0
+
+log_test() { # $1 actual_rc $2 expected_rc $3 name
+ if [ "$1" = "$2" ]; then
+ printf "TEST: %-58s [ OK ]\n" "$3"
+ else
+ printf "TEST: %-58s [FAIL] (rc=$1 want=$2)\n" "$3"
+ ret=1
+ fi
+}
+
+# passes (returns 0) iff the command FAILS
+expect_fail() {
+ if "$@" >/dev/null 2>&1; then return 1; else return 0; fi
+}
+
+cleanup() { ip netns del "$NS" 2>/dev/null; }
+
+command -v ip >/dev/null 2>&1 || { echo "SKIP: iproute2 not found"; exit $ksft_skip; }
+ip nexthop help 2>&1 | grep -q fdb || { echo "SKIP: no fdb nexthop support"; exit $ksft_skip; }
+
+trap cleanup EXIT
+cleanup
+ip netns add "$NS" || { echo "SKIP: cannot create netns"; exit $ksft_skip; }
+$IP link set lo up
+
+# Probe for "port" keyword + kernel NHA_FDB_PORT support; SKIP if missing.
+if ! $IP nexthop add id 1 via 10.0.0.1 fdb port 4790 2>/dev/null; then
+ echo "SKIP: 'ip nexthop ... fdb port' unsupported (needs patched kernel + iproute2)"
+ exit $ksft_skip
+fi
+log_test 0 0 "add fdb nexthop with port"
+
+# Dump roundtrip must echo the port back.
+$IP nexthop show id 1 | grep -qw "port 4790"
+log_test $? 0 "dump shows fdb port 4790"
+
+# Reject: port on a routed (non-fdb) nexthop.
+expect_fail $IP nexthop add id 2 via 10.0.0.1 dev lo port 4790
+log_test $? 0 "reject port on non-fdb nexthop"
+
+# Reject: fdb port without a gateway.
+expect_fail $IP nexthop add id 3 fdb port 4790
+log_test $? 0 "reject fdb port without gateway"
+
+# The HA case: a group whose legs share the gateway but differ in port.
+$IP nexthop add id 10 via 10.0.0.1 fdb port 4789 && \
+$IP nexthop add id 11 via 10.0.0.1 fdb port 5789 && \
+$IP nexthop add id 100 group 10/11 fdb
+log_test $? 0 "add fdb nexthop group with differing ports"
+
+# A fdb nexthop without a port must NOT emit one (backward compat).
+$IP nexthop add id 20 via 10.0.0.1 fdb
+$IP nexthop show id 20 | grep -qw "port"
+log_test $? 1 "fdb nexthop without port omits NHA_FDB_PORT"
+
+if [ $ret -eq 0 ]; then
+ echo "PASS: all NHA_FDB_PORT control-plane checks"
+else
+ echo "FAIL: one or more NHA_FDB_PORT checks failed"
+fi
+exit $ret
--
2.43.0
^ permalink raw reply related
* [PATCH net-next 2/3] vxlan: honor per-nexthop fdb destination port
From: Jack Ma @ 2026-07-12 19:12 UTC (permalink / raw)
To: netdev, David Ahern
Cc: Ido Schimmel, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Shuah Khan, linux-kselftest,
linux-kernel, Jack Ma
In-Reply-To: <20260712191218.236-1-jack4it@gmail.com>
When an fdb entry points at a nexthop group, vxlan_fdb_nh_path_select()
resolves the selected leg's remote IP but leaves the UDP destination port
at the device default (vxlan->cfg.dst_port).
Extend nexthop_path_fdb_result() to also return the selected nexthop's
NHA_FDB_PORT (0 when unset) and have vxlan_fdb_nh_path_select() store it
in rdst->remote_port. vxlan_xmit_one() already prefers rdst->remote_port
when non-zero and falls back to the device port otherwise, so nexthops
without a port are unaffected.
This lets one fdb nexthop group load-balance a flow across legs that
share an underlay IP but differ in UDP destination port.
Signed-off-by: Jack Ma <jack4it@gmail.com>
---
include/net/nexthop.h | 5 ++++-
include/net/vxlan.h | 5 ++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/include/net/nexthop.h b/include/net/nexthop.h
index 9c8227996..7fb612a73 100644
--- a/include/net/nexthop.h
+++ b/include/net/nexthop.h
@@ -576,7 +576,8 @@ struct fib_nh_common *nexthop_fdb_nhc(struct nexthop *nh)
}
static inline struct fib_nh_common *nexthop_path_fdb_result(struct nexthop *nh,
- int hash)
+ int hash,
+ __be16 *fdb_port)
{
struct nh_info *nhi;
struct nexthop *nhp;
@@ -585,6 +586,8 @@ static inline struct fib_nh_common *nexthop_path_fdb_result(struct nexthop *nh,
if (unlikely(!nhp))
return NULL;
nhi = rcu_dereference(nhp->nh_info);
+ if (fdb_port)
+ *fdb_port = nhi->fdb_port;
return &nhi->fib_nhc;
}
#endif
diff --git a/include/net/vxlan.h b/include/net/vxlan.h
index dfba89695..de41b3746 100644
--- a/include/net/vxlan.h
+++ b/include/net/vxlan.h
@@ -567,8 +567,9 @@ static inline bool vxlan_fdb_nh_path_select(struct nexthop *nh,
struct vxlan_rdst *rdst)
{
struct fib_nh_common *nhc;
+ __be16 fdb_port = 0;
- nhc = nexthop_path_fdb_result(nh, hash >> 1);
+ nhc = nexthop_path_fdb_result(nh, hash >> 1, &fdb_port);
if (unlikely(!nhc))
return false;
@@ -583,6 +584,8 @@ static inline bool vxlan_fdb_nh_path_select(struct nexthop *nh,
break;
}
+ rdst->remote_port = fdb_port;
+
return true;
}
--
2.43.0
^ permalink raw reply related
* [PATCH net-next 1/3] net: nexthop: add NHA_FDB_PORT for fdb nexthops
From: Jack Ma @ 2026-07-12 19:12 UTC (permalink / raw)
To: netdev, David Ahern
Cc: Ido Schimmel, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Shuah Khan, linux-kselftest,
linux-kernel, Jack Ma
In-Reply-To: <20260712191218.236-1-jack4it@gmail.com>
Commit 1274e1cc4226 ("vxlan: ecmp support for mac fdb entries") lets a
single inner MAC be reached through a group of remote VTEPs, with the
kernel flow-hashing across the group members. Each member carries its
own remote IP, but the UDP destination port is always taken from the
VXLAN device (vxlan->cfg.dst_port) and cannot be set per member.
Some deployments pack several receivers behind one underlay IP and tell
them apart by UDP port, so they need a per-nexthop destination port to
spread flows across (IP, port) tuples rather than IP alone.
Add a netlink attribute NHA_FDB_PORT (__be16, mirroring NDA_PORT) that
carries an optional UDP destination port on an fdb nexthop. It is only
accepted together with NHA_FDB and NHA_GATEWAY; it is stored in struct
nh_info and echoed back on dump. This patch is control-plane plumbing
only; the VXLAN datapath is wired up in a follow-up patch, so behaviour
is unchanged for now.
Signed-off-by: Jack Ma <jack4it@gmail.com>
---
include/net/nexthop.h | 2 ++
include/uapi/linux/nexthop.h | 3 +++
net/ipv4/nexthop.c | 20 +++++++++++++++++++-
3 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/include/net/nexthop.h b/include/net/nexthop.h
index 572e69cda..9c8227996 100644
--- a/include/net/nexthop.h
+++ b/include/net/nexthop.h
@@ -28,6 +28,7 @@ struct nh_config {
u8 nh_protocol;
u8 nh_blackhole;
u8 nh_fdb;
+ __be16 nh_fdb_port;
u32 nh_flags;
int nh_ifindex;
@@ -63,6 +64,7 @@ struct nh_info {
u8 family;
bool reject_nh;
bool fdb_nh;
+ __be16 fdb_port;
union {
struct fib_nh_common fib_nhc;
diff --git a/include/uapi/linux/nexthop.h b/include/uapi/linux/nexthop.h
index bc49baf4a..e587bbf3b 100644
--- a/include/uapi/linux/nexthop.h
+++ b/include/uapi/linux/nexthop.h
@@ -83,6 +83,9 @@ enum {
/* u32; read-only; whether any driver collects HW stats */
NHA_HW_STATS_USED,
+ /* be16; UDP destination port for an fdb nexthop (e.g. VXLAN) */
+ NHA_FDB_PORT,
+
__NHA_MAX,
};
diff --git a/net/ipv4/nexthop.c b/net/ipv4/nexthop.c
index 6205bd57a..5b27cc9a9 100644
--- a/net/ipv4/nexthop.c
+++ b/net/ipv4/nexthop.c
@@ -39,6 +39,7 @@ static const struct nla_policy rtm_nh_policy_new[] = {
[NHA_ENCAP_TYPE] = { .type = NLA_U16 },
[NHA_ENCAP] = { .type = NLA_NESTED },
[NHA_FDB] = { .type = NLA_FLAG },
+ [NHA_FDB_PORT] = { .type = NLA_U16 },
[NHA_RES_GROUP] = { .type = NLA_NESTED },
[NHA_HW_STATS_ENABLE] = NLA_POLICY_MAX(NLA_U32, true),
};
@@ -956,6 +957,9 @@ static int nh_fill_node(struct sk_buff *skb, struct nexthop *nh,
} else if (nhi->fdb_nh) {
if (nla_put_flag(skb, NHA_FDB))
goto nla_put_failure;
+ if (nhi->fdb_port &&
+ nla_put_be16(skb, NHA_FDB_PORT, nhi->fdb_port))
+ goto nla_put_failure;
} else {
const struct net_device *dev;
@@ -1055,6 +1059,9 @@ static size_t nh_nlmsg_size_single(struct nexthop *nh)
break;
}
+ if (nhi->fdb_nh)
+ sz += nla_total_size(2); /* NHA_FDB_PORT */
+
if (nhi->fib_nhc.nhc_lwtstate) {
sz += lwtunnel_get_encap_size(nhi->fib_nhc.nhc_lwtstate);
sz += nla_total_size(2); /* NHA_ENCAP_TYPE */
@@ -2956,8 +2963,10 @@ static struct nexthop *nexthop_create(struct net *net, struct nh_config *cfg,
nhi->family = cfg->nh_family;
nhi->fib_nhc.nhc_scope = RT_SCOPE_LINK;
- if (cfg->nh_fdb)
+ if (cfg->nh_fdb) {
nhi->fdb_nh = 1;
+ nhi->fdb_port = cfg->nh_fdb_port;
+ }
if (cfg->nh_blackhole) {
nhi->reject_nh = 1;
@@ -3147,6 +3156,15 @@ static int rtm_to_nh_config(struct net *net, struct sk_buff *skb,
cfg->nh_fdb = nla_get_flag(tb[NHA_FDB]);
}
+ if (tb[NHA_FDB_PORT]) {
+ if (!tb[NHA_FDB] || !tb[NHA_GATEWAY]) {
+ NL_SET_ERR_MSG(extack,
+ "FDB port can only be set on fdb nexthops that have a gateway");
+ goto out;
+ }
+ cfg->nh_fdb_port = nla_get_be16(tb[NHA_FDB_PORT]);
+ }
+
if (tb[NHA_GROUP]) {
if (nhm->nh_family != AF_UNSPEC) {
NL_SET_ERR_MSG(extack, "Invalid family for group");
--
2.43.0
^ permalink raw reply related
* [PATCH net-next 0/3] net: nexthop: per-nexthop UDP dst port for fdb (VXLAN) nexthops
From: Jack Ma @ 2026-07-12 19:12 UTC (permalink / raw)
To: netdev, David Ahern
Cc: Ido Schimmel, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Shuah Khan, linux-kselftest,
linux-kernel, Jack Ma
FDB nexthops let a VXLAN fdb entry point at a group of remote VTEPs, with the
kernel flow-hashing across the group (commit 1274e1cc4226 ("vxlan: ecmp support
for mac fdb entries")). Each leg carries its own remote IP, but the UDP
destination port is always taken from the VXLAN device (vxlan->cfg.dst_port)
and cannot be set per leg.
Some deployments pack several receivers behind a single underlay IP and tell
them apart by UDP destination port. To spread flows across such receivers they
need a nexthop group whose legs share the remote IP but differ in UDP port,
which is not currently expressible.
This series adds an optional per-nexthop UDP destination port for fdb nexthops:
- Patch 1 adds a netlink attribute NHA_FDB_PORT (__be16, mirroring NDA_PORT),
stored in struct nh_info and echoed back on dump. It is
only accepted together with NHA_FDB and NHA_GATEWAY. Control-plane only;
datapath behaviour is unchanged.
- Patch 2 wires it into the VXLAN datapath: vxlan_fdb_nh_path_select() sets
rdst->remote_port to the selected leg's port. vxlan_xmit_one() already
prefers rdst->remote_port when non-zero and otherwise falls back to the
device port, so nexthops without a port are unaffected (backward
compatible).
- Patch 3 adds a selftest.
Example:
ip nexthop add id 1 via 192.0.2.10 fdb port 4789
ip nexthop add id 2 via 192.0.2.10 fdb port 5789
ip nexthop add id 10 group 1/2 fdb
bridge fdb add 00:11:22:33:44:55 dev vxlan0 nhid 10
Both legs share gateway 192.0.2.10 and differ only in UDP port; the kernel
hashes flows across them.
Testing: kernel and iproute2 built on net-next. The control-plane selftest
passes (6/6) and a datapath test (two netns, tcpdump on the underlay) confirms
outer traffic is hashed across both UDP ports. The series is bisectable:
patches 1 and 2 each build individually.
Note: remote_vni has the same per-leg limitation and could be exposed the same
way (e.g. NHA_FDB_VNI); left as a follow-up to keep this series focused.
Naming NHA_FDB_PORT vs a generic NHA_PORT is open to discussion -- VXLAN is the
only fdb-nexthop consumer today.
A matching iproute2 change (the `ip nexthop ... fdb port N` keyword) is posted
separately to the iproute2 list.
Jack Ma (3):
net: nexthop: add NHA_FDB_PORT for fdb nexthops
vxlan: honor per-nexthop fdb destination port
selftests: net: add coverage for fdb nexthop dst port
include/net/nexthop.h | 7 +-
include/net/vxlan.h | 5 +-
include/uapi/linux/nexthop.h | 3 +
net/ipv4/nexthop.c | 20 ++++-
tools/testing/selftests/net/Makefile | 1 +
.../selftests/net/fib_nexthops_fdb_port.sh | 78 +++++++++++++++++++
6 files changed, 111 insertions(+), 3 deletions(-)
create mode 100755 tools/testing/selftests/net/fib_nexthops_fdb_port.sh
base-commit: f6f3b36c15ed44de1fbb44e645e4fae8c4a4453e
--
2.43.0
^ permalink raw reply
* [PATCH net] net: rnpgbe: Pass an expression directly in rnpgbe_rm_adapter()
From: Markus Elfring @ 2026-07-12 18:35 UTC (permalink / raw)
To: netdev, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, MD Danish Anwar, Michael Grzeschik, Paolo Abeni,
Uwe Kleine-König, Vadim Fedorenko, Yibo Dong
Cc: LKML, kernel-janitors
From: Markus Elfring <elfring@users.sourceforge.net>
Date: Sun, 12 Jul 2026 20:25:10 +0200
The address of a data structure member was determined before
a corresponding null pointer check in the implementation of
the function “rnpgbe_rm_adapter”.
Thus avoid the risk for undefined behaviour by omitting the variable “hw”.
Pass the required address directly to a function call.
This issue was detected by using the Coccinelle software.
Fixes: 2ee95ec17e97c58b65e978a08b75fa8cb6424e4e ("net: rnpgbe: Add register_netdev")
Cc: stable@vger.kernel.org
Signed-off-by: Markus Elfring <elfring@users.sourceforge.net>
---
drivers/net/ethernet/mucse/rnpgbe/rnpgbe_main.c | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/drivers/net/ethernet/mucse/rnpgbe/rnpgbe_main.c b/drivers/net/ethernet/mucse/rnpgbe/rnpgbe_main.c
index 70a2b0082ba8..93cf757c951c 100644
--- a/drivers/net/ethernet/mucse/rnpgbe/rnpgbe_main.c
+++ b/drivers/net/ethernet/mucse/rnpgbe/rnpgbe_main.c
@@ -245,7 +245,6 @@ static int rnpgbe_probe(struct pci_dev *pdev, const struct pci_device_id *id)
static void rnpgbe_rm_adapter(struct pci_dev *pdev)
{
struct mucse *mucse = pci_get_drvdata(pdev);
- struct mucse_hw *hw = &mucse->hw;
struct net_device *netdev;
int err;
@@ -253,7 +252,7 @@ static void rnpgbe_rm_adapter(struct pci_dev *pdev)
return;
netdev = mucse->netdev;
unregister_netdev(netdev);
- err = rnpgbe_send_notify(hw, false, mucse_fw_powerup);
+ err = rnpgbe_send_notify(&mucse->hw, false, mucse_fw_powerup);
if (err)
dev_warn(&pdev->dev, "Send powerdown to hw failed %d\n", err);
free_netdev(netdev);
--
2.55.0
^ permalink raw reply related
* Re: [PATCH net-next v6 2/2] net: dsa: realtek: rtl8365mb: add HSGMII support for RTL8367S
From: Mieczyslaw Nalewaj @ 2026-07-12 18:05 UTC (permalink / raw)
To: contact, Linus Walleij, Alvin Šipraga, Andrew Lunn,
Vladimir Oltean, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Russell King
Cc: Maxime Chevallier, Luiz Angelo Daros de Luca, netdev,
linux-kernel
In-Reply-To: <20260711-rtl8367s-sgmii-v6-2-88f7944ddca7@c127.dev>
Hi Johan
On 7/12/2026 6:31 AM, Johan Alvarado via B4 Relay wrote:
> From: Johan Alvarado <contact@c127.dev>
>
> In addition to SGMII, the RTL8367S SerDes also supports HSGMII, which
> carries 2.5 Gbps with the same signaling as SGMII at 2.5x clock rate.
> The chip info table already declares HSGMII as a supported interface
> mode for external interface 1.
>
> Extend the SerDes PCS to handle HSGMII, which phylink represents as
> 2500base-x:
>
> - Select the HSGMII SerDes tuning parameters and external interface
> mode, and mux the SerDes to MAC8 in HSGMII mode, from pcs_config()
> according to the interface. The parameters are again lifted from the
> GPL-licensed Realtek rtl8367c vendor driver, and again only cover
> the tuning variant for a non-zero chip option, so the mode is gated
> on the option probed at setup.
>
> - Advertise 2500base-x and MAC_2500FD on ports whose external
> interface supports HSGMII.
>
> - Accept SPEED_2500 in the forced link configuration. The MAC speed
> field has no 2.5 Gbps value: the rate is determined by the HSGMII
> SerDes configuration, and the vendor driver programs the 1 Gbps
> value here, so do the same.
>
> - Raise the port 6 ingress and egress rate limiters to their maximum
> at setup time, as the vendor switch init does unconditionally for
> the whole chip family. The chip resets them to 0x1FFFF (~1.048 Gbps
> in units of 8 Kbps), which caps the aggregate HSGMII throughput at
> roughly 1 Gbps. The vendor documentation describes the reset
> default as disabling the limiter, but the cap is real: on an
> RTL8367S-based Mercusys MR85X running an OpenWrt backport of this
> series, several clients on 1 Gbps user ports were limited to about
> 1.02 Gbps combined across the HSGMII CPU port until these limiters
> were raised, after which throughput reached about 2 Gbps [1]. The
> related HSGMII scheduler line rate (LINE_RATE_HSG_H) is already set
> to its maximum by the common init jam table.
>
> Tested on a Mercusys MR80X v2.20, where the RTL8367S is connected to
> the SoC over HSGMII.
>
> Link: https://github.com/openwrt/openwrt/pull/19445#issuecomment-4505613294 [1]
> Suggested-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
> Suggested-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
> Signed-off-by: Johan Alvarado <contact@c127.dev>
Thanks for the patch, this matches what I've seen too on similar
hardware.
As discussed earlier in the thread: the rate limiter helper, as well
as pcs_config()/RTL8365MB_SDS_BYPASS_LINE_RATE_MASK, currently assume
the SerDes is always muxed to external interface 1 / port 6
(RTL8365MB_SDS_EXT_INTERFACE_ID / _PORT). That's true for every chip
currently in rtl8365mb_chip_infos[] (RTL8367S and RTL8367SB both have
their SGMII/HSGMII-capable extint at { 6, 1, ... }), so nothing is
broken today.
I still think it's worth guarding against this ahead of time though.
It isn't an architectural constant of the family, just something that
happens to hold for the two chips currently in the table, and there's
no guarantee a future chip won't mux its SerDes to a different port
or extint id. If that happens, pcs_config() would silently program
the wrong DIGITAL_INTERFACE_SELECT/FORCE register, and the rate
limiter helper would write to the wrong port's registers (or need to
guess an address it can't verify) - the kind of thing that's easy to
miss in review since it only breaks on hardware nobody in this thread
has to test with.
Since rtl8365mb_sds_probe_option() already walks
chip_info->extints[] looking for the SGMII/HSGMII-capable one, I'd
like to have it cache the discovered port and id (mb->sds_port /
mb->sds_id) instead of relying on the fixed
RTL8365MB_SDS_EXT_INTERFACE_ID/_PORT. pcs_config() and the bypass
line-rate mask would then use the cached values, and
rtl8365mb_sds_raise_rate_limits() would check mb->sds_port == 6 and
warn+skip instead of assuming, since only the port 6 register
addresses are known/verified so far.
Draft below, as a preparatory patch to go before this one. No
functional change for RTL8367S/RTL8367SB - just removes the
assumption for whatever comes next. Let me know what you think.
---
#define RTL8365MB_BYPASS_LINE_RATE_REG 0x03F7
#define RTL8365MB_BYPASS_LINE_RATE_MASK(_port) BIT((_port) - 5)
+/* Port 6 ingress and egress rate limiter registers. Each limit is a 19-bit
+ * value in units of 8 Kbps, split across a 16-bit LSB register (CTRL0) and a
+ * 3-bit MSB field (CTRL1). The chip resets them to 0x1FFFF; see
-+ * rtl8365mb_sds_raise_rate_limits().
++ * rtl8365mb_sds_raise_rate_limits(). Only port 6 addresses are known - every
++ * chip currently supported muxes the SerDes to port 6 (see mb->sds_port),
++ * but this is not guaranteed for chips that may be added later.
+ */
+#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL0_REG 0x00CF
+#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_REG 0x00D0
@@ -30,7 +32,29 @@
/* SerDes indirect access registers */
#define RTL8365MB_SDS_INDACS_CMD_REG 0x6600
#define RTL8365MB_SDS_INDACS_CMD_BUSY_MASK 0x0100
-@@ -637,6 +649,18 @@ static const struct rtl8365mb_jam_tbl_entry rtl8365mb_sds_jam_sgmii[] = {
+@@ -270,12 +284,11 @@
+ #define RTL8365MB_SDS_MISC_SGMII_SPD_MASK 0x0180
+ #define RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK 0x0040
+
+-/* The embedded SerDes can only be muxed to external interface 1 (MAC8),
+- * which is port 6.
++/* The embedded SerDes is muxed to whichever external interface the chip's
++ * extint table marks as SGMII/HSGMII-capable. For every chip currently
++ * supported this is external interface 1 (MAC8) on port 6, but this is
++ * discovered per-chip in rtl8365mb_sds_probe_option() and cached in
++ * mb->sds_port / mb->sds_id rather than assumed, since it need not hold for
++ * chips added in the future.
+ */
+-#define RTL8365MB_SDS_EXT_INTERFACE_ID 1
+-#define RTL8365MB_SDS_EXT_INTERFACE_PORT 6
+-
+-/* Line rate bypass bit for the SerDes external interface */
+-#define RTL8365MB_SDS_BYPASS_LINE_RATE_MASK \
+- RTL8365MB_BYPASS_LINE_RATE_MASK(RTL8365MB_SDS_EXT_INTERFACE_PORT)
+
+ /* SerDes tuning parameter variant selector. The vendor driver picks between
+ * two sets of SerDes tuning parameters based on this chip option. Reading it
+@@ -637,6 +650,18 @@ static const struct rtl8365mb_jam_tbl_entry rtl8365mb_sds_jam_sgmii[] = {
{ 0x0424, 0xD810 }, { 0x002E, 0x83F2 },
};
@@ -49,25 +73,61 @@
enum rtl8365mb_phy_interface_mode {
RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
-@@ -1242,20 +1266,70 @@ static int rtl8365mb_sds_probe_option(struct realtek_priv *priv)
+@@ -871,6 +896,11 @@ struct rtl8365mb {
+ struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
+ struct phylink_pcs pcs;
+ bool sds_supported;
++ /* Port and extint id the SerDes is muxed to on this chip, discovered
++ * in rtl8365mb_sds_probe_option() from the chip's extint table.
++ * Valid only if sds_supported is true.
++ */
++ int sds_port;
++ int sds_id;
+ };
+
+ #define pcs_to_rtl8365mb(_pcs) container_of((_pcs), struct rtl8365mb, pcs)
+@@ -1229,6 +1259,9 @@ static int rtl8365mb_sds_probe_option(struct realtek_priv *priv)
+ if (i == RTL8365MB_MAX_NUM_EXTINTS)
+ return 0;
+
++ mb->sds_port = extint->port;
++ mb->sds_id = extint->id;
++
+ ret = regmap_write(priv->map, RTL8365MB_SDS_OPTION_ARM_REG,
+ RTL8365MB_SDS_OPTION_ARM_KEY);
+ if (ret)
+@@ -1242,20 +1275,74 @@ static int rtl8365mb_sds_probe_option(struct realtek_priv *priv)
return 0;
}
-+/* The vendor driver raises the port 6 ingress and egress rate limiters to
-+ * their maximum in its switch init, unconditionally for the whole chip
-+ * family. The chip reset in rtl8365mb_setup() puts them back to their reset
-+ * default of 0x1FFFF, a ~1.048 Gbps limit which caps the aggregate
-+ * throughput of an HSGMII CPU port at roughly 1 Gbps. The vendor
++/* The vendor driver raises the ingress and egress rate limiters of the
++ * SerDes-connected port to their maximum in its switch init, unconditionally
++ * for the whole chip family. The chip reset in rtl8365mb_setup() puts them
++ * back to their reset default of 0x1FFFF, a ~1.048 Gbps limit which caps the
++ * aggregate throughput of an HSGMII CPU port at roughly 1 Gbps. The vendor
+ * documentation describes the reset default as disabling the limiter, but
+ * the cap has been observed on hardware. Raise them likewise, to 0x7FFFF
+ * (~4.19 Gbps, above the HSGMII line rate). The related HSGMII scheduler
+ * line rate register (LINE_RATE_HSG_H, 0x03FA) is already set to its
+ * maximum by the common init jam table.
++ *
++ * Only the port 6 register addresses are known. Every chip currently
++ * supported muxes the SerDes to port 6 (see mb->sds_port, discovered in
++ * rtl8365mb_sds_probe_option()); if a future chip ends up muxing it
++ * elsewhere, skip rather than guess at an address.
+ */
+static int rtl8365mb_sds_raise_rate_limits(struct realtek_priv *priv)
+{
++ struct rtl8365mb *mb = priv->chip_data;
+ int ret;
+
++ if (mb->sds_port != 6) {
++ dev_warn(priv->dev,
++ "SerDes on port %d has no known rate limiter registers, skipping\n",
++ mb->sds_port);
++ return 0;
++ }
++
+ ret = regmap_write(priv->map, RTL8365MB_INGRESSBW_PORT6_RATE_CTRL0_REG,
+ 0xFFFF);
+ if (ret)
@@ -96,17 +156,18 @@
bool permit_pause_to_mac)
{
+ const struct rtl8365mb_jam_tbl_entry *sds_jam;
- const int id = RTL8365MB_SDS_EXT_INTERFACE_ID;
struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
struct realtek_priv *priv;
+ size_t sds_jam_size;
++ int id;
+ u32 mode;
u16 val;
int ret;
int i;
priv = mb->priv;
-
++ id = mb->sds_id;
++
+ if (interface == PHY_INTERFACE_MODE_2500BASEX) {
+ sds_jam = rtl8365mb_sds_jam_hsgmii;
+ sds_jam_size = ARRAY_SIZE(rtl8365mb_sds_jam_hsgmii);
@@ -116,8 +177,15 @@
/* Hold the embedded DW8051 microcontroller in reset and keep it
* disabled. The vendor driver loads firmware into it to manage the
- * SerDes link, but the firmware only duplicates work that phylink
-@@ -1283,24 +1357,24 @@ static int rtl8365mb_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
+@@ -1272,17 +1359,20 @@ static int rtl8365mb_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
+ /* The vendor driver clears the line rate bypass for all interface
+ * modes except TMII.
+ */
+ ret = regmap_update_bits(priv->map, RTL8365MB_BYPASS_LINE_RATE_REG,
+- RTL8365MB_SDS_BYPASS_LINE_RATE_MASK, 0);
++ RTL8365MB_BYPASS_LINE_RATE_MASK(mb->sds_port),
++ 0);
+ if (ret)
return ret;
/* Tune the SerDes with vendor-prescribed parameters */
^ permalink raw reply
* Re: [PATCH net-next v4 3/3] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
From: Jori Koolstra @ 2026-07-12 18:23 UTC (permalink / raw)
To: Christian Brauner
Cc: Aleksa Sarai, Kuniyuki Iwashima, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, netdev, linux-fsdevel,
linux-kernel
In-Reply-To: <20260707-sinnieren-versichern-heuballen-e86897d216c3@brauner>
> Op 07-07-2026 13:02 CEST schreef Christian Brauner <brauner@kernel.org>:
>
> > +
> > +TEST_F(scm_rights_denial_bpf, all_allowed)
> > +{
> > + int slots[NR_FILES], nr_slots, flags, i;
> > +
> > + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
> > + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> > + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> > +
> > + ASSERT_EQ(NR_FILES, nr_slots);
> > + EXPECT_EQ(0, flags & MSG_CTRUNC);
> > +
> > + for (i = 0; i < NR_FILES; i++) {
> > + ASSERT_LE(0, slots[i]);
>
> Why do you assert less-or-equal? You want a valid fd so you should
> expect >= 0?
>
This says 0 <= slots[i], or equivalently slots[i] >= 0.
> > +
> > +TEST_F(scm_rights_denial_bpf, denied_without_notrunc)
> > +{
> > + int slots[NR_FILES], nr_slots, flags;
> > +
> > + /*
> > + * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is
> > + * truncated at the first denied fd and MSG_CTRUNC is set.
> > + */
> > + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1]));
> > +
> > + ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
> > + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
> > +
> > + ASSERT_EQ(1, nr_slots);
> > + EXPECT_NE(0, flags & MSG_CTRUNC);
> > +
> > + ASSERT_LE(0, slots[0]);
>
> Why do you expect less-equal than zero? Don't you need ASSERT_GE()
> because you want slots[0] to be a valid file desscriptor?
>
Idem.
But I do think ASSERT_GE() read a bit better here, so I have changed it.
> And even the other way around... Zero is a valid file descriptor so on
> failure you must expect ASSERT_LT()?
>
> --
> Christian Brauner <brauner@kernel.org>
Thanks,
Jori.
^ permalink raw reply
* RE: [PATCH ethtool-next v2 2/2] sfpid: print all compliance codes
From: Danielle Ratson @ 2026-07-12 18:16 UTC (permalink / raw)
To: Aleksander Jan Bajkowski, mkubecek@suse.cz, andrew@lunn.ch,
davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, jbe@pengutronix.de, netdev@vger.kernel.org
In-Reply-To: <20260711095803.257213-2-olek2@wp.pl>
> -----Original Message-----
> From: Aleksander Jan Bajkowski <olek2@wp.pl>
> Sent: Saturday, 11 July 2026 12:54
> To: Danielle Ratson <danieller@nvidia.com>; mkubecek@suse.cz;
> andrew@lunn.ch; davem@davemloft.net; edumazet@google.com;
> kuba@kernel.org; pabeni@redhat.com; jbe@pengutronix.de;
> netdev@vger.kernel.org
> Cc: Aleksander Jan Bajkowski <olek2@wp.pl>
> Subject: [PATCH ethtool-next v2 2/2] sfpid: print all compliance codes
>
> SFP modules implement multiple compliance codes. This is common for dual-
> rate modules. Before the `json` option was introduced, all compliance codes
> were displayed. Currently, only the last code is displayed. This commit fixes
> that bug. Compliance codes are represented as array.
>
> Before:
> $ ethtool -m sfp-wan
> ...
> Transceiver codes : 0x00 0x00 0x00 0x01 0x20 0x40 0x0c
> 0x15 0x00
> Transceiver type : FC: 100 MBytes/sec
> ...
> $ ethtool --json -m sfp-wan
> [ {
> ...
> "transceiver_codes": [ 0,0,0,1,32,64,12,21,0 ],
> "transceiver_type": "FC: 100 MBytes/sec", ...
> } ]
>
> After:
> $ ethtool -m sfp-wan
> ...
> Transceiver codes : 0x00 0x00 0x00 0x01 0x20 0x40 0x0c
> 0x15 0x00
> Transceiver type : Ethernet: 1000BASE-SX
> Transceiver type : FC: intermediate distance (I)
> Transceiver type : FC: Shortwave laser w/o OFC (SN)
> Transceiver type : FC: Multimode, 62.5um (M6)
> Transceiver type : FC: Multimode, 50um (M5)
> Transceiver type : FC: 400 MBytes/sec
> Transceiver type : FC: 200 MBytes/sec
> Transceiver type : FC: 100 MBytes/sec
> ...
> $ ethtool --json -m sfp-wan
> [ {
> ...
> "transceiver_codes": [ 0,0,0,1,32,64,12,21,0 ],
> "transceiver_type": [ "Ethernet: 1000BASE-SX","FC: intermediate distance
> (I)","FC: Shortwave laser w/o OFC (SN)","FC: Multimode, 62.5um (M6)","FC:
> Multimode, 50um (M5)","FC: 400 MBytes/sec","FC: 200 MBytes/sec","FC:
> 100 MBytes/sec" ], ...
> } ]
>
> Fixes: 4071862f58d8 ("sfpid: Add JSON output handling to --module-info in
> SFF8079 modules")
> Signed-off-by: Aleksander Jan Bajkowski <olek2@wp.pl>
> ---
> Changes in v2:
> - drop </pre> leftover
> - use single sfp module in Before/After
> - rename module_print_array_string() ->
> module_print_any_array_string_entry()
> ---
> sfpid.c | 291 ++++++++++++++++++++++++++++++++++++--------------------
> 1 file changed, 187 insertions(+), 104 deletions(-)
>
> diff --git a/sfpid.c b/sfpid.c
It seems like qsfp suffers from the same exact issue in the equivalent sff8636_show_transceiver(). I think it is worth to address that too since it fixes the same issue.
> index ec5dd95..25ddd57 100644
> --- a/sfpid.c
> +++ b/sfpid.c
> @@ -50,7 +50,6 @@ static void sff8079_show_connector(const __u8 *id)
> static void sff8079_show_transceiver(const __u8 *id) {
> static const char *pfx = "Transceiver type";
> - char value[140] = "";
>
> if (is_json_context()) {
> open_json_array("transceiver_codes", ""); @@ -70,242
> +69,326 @@ static void sff8079_show_transceiver(const __u8 *id)
> "Transceiver codes", id[3], id[4], id[5], id[6],
> id[7], id[8], id[9], id[10], id[36]);
> }
> +
> + if (is_json_context())
> + open_json_array("transceiver_type", "");
> +
> /* 10G Ethernet Compliance Codes */
> if (id[3] & (1 << 7))
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
Indentation again, please run checkpatch to address all the errors.
> "10G Ethernet: 10G Base-ER [SFF-8472 rev10.4
> onwards]");
> if (id[3] & (1 << 6))
> - sprintf(value, "%s", "10G Ethernet: 10G Base-LRM");
> + module_print_any_array_string_entry(pfx,
> + "10G Ethernet: 10G Base-LRM");
> if (id[3] & (1 << 5))
> - sprintf(value, "%s", "10G Ethernet: 10G Base-LR");
> + module_print_any_array_string_entry(pfx,
> + "10G Ethernet: 10G Base-LR");
> if (id[3] & (1 << 4))
> - sprintf(value, "%s", "10G Ethernet: 10G Base-SR");
> + module_print_any_array_string_entry(pfx,
> + "10G Ethernet: 10G Base-SR");
> /* Infiniband Compliance Codes */
> if (id[3] & (1 << 3))
> - sprintf(value, "%s", "Infiniband: 1X SX");
> + module_print_any_array_string_entry(pfx,
> + "Infiniband: 1X SX");
> if (id[3] & (1 << 2))
> - sprintf(value, "%s", "Infiniband: 1X LX");
> + module_print_any_array_string_entry(pfx,
> + "Infiniband: 1X LX");
> if (id[3] & (1 << 1))
> - sprintf(value, "%s", "Infiniband: 1X Copper Active");
> + module_print_any_array_string_entry(pfx,
> + "Infiniband: 1X Copper Active");
> if (id[3] & (1 << 0))
> - sprintf(value, "%s", "Infiniband: 1X Copper Passive");
> + module_print_any_array_string_entry(pfx,
> + "Infiniband: 1X Copper Passive");
> /* ESCON Compliance Codes */
> if (id[4] & (1 << 7))
> - sprintf(value, "%s", "ESCON: ESCON MMF, 1310nm LED");
> + module_print_any_array_string_entry(pfx,
> + "ESCON: ESCON MMF, 1310nm LED");
> if (id[4] & (1 << 6))
> - sprintf(value, "%s", "ESCON: ESCON SMF, 1310nm Laser");
> + module_print_any_array_string_entry(pfx,
> + "ESCON: ESCON SMF, 1310nm Laser");
> /* SONET Compliance Codes */
> if (id[4] & (1 << 5))
> - sprintf(value, "%s", "SONET: OC-192, short reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-192, short reach");
> if (id[4] & (1 << 4))
> - sprintf(value, "%s", "SONET: SONET reach specifier bit 1");
> + module_print_any_array_string_entry(pfx,
> + "SONET: SONET reach specifier bit 1");
> if (id[4] & (1 << 3))
> - sprintf(value, "%s", "SONET: SONET reach specifier bit 2");
> + module_print_any_array_string_entry(pfx,
> + "SONET: SONET reach specifier bit 2");
> if (id[4] & (1 << 2))
> - sprintf(value, "%s", "SONET: OC-48, long reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-48, long reach");
> if (id[4] & (1 << 1))
> - sprintf(value, "%s", "SONET: OC-48, intermediate reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-48, intermediate reach");
> if (id[4] & (1 << 0))
> - sprintf(value, "%s", "SONET: OC-48, short reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-48, short reach");
> if (id[5] & (1 << 6))
> - sprintf(value, "%s", "SONET: OC-12, single mode, long reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-12, single mode, long reach");
> if (id[5] & (1 << 5))
> - sprintf(value, "%s", "SONET: OC-12, single mode, inter.
> reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-12, single mode, inter. reach");
> if (id[5] & (1 << 4))
> - sprintf(value, "%s", "SONET: OC-12, short reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-12, short reach");
> if (id[5] & (1 << 2))
> - sprintf(value, "%s", "SONET: OC-3, single mode, long reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-3, single mode, long reach");
> if (id[5] & (1 << 1))
> - sprintf(value, "%s", "SONET: OC-3, single mode, inter. reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-3, single mode, inter. reach");
> if (id[5] & (1 << 0))
> - sprintf(value, "%s", "SONET: OC-3, short reach");
> + module_print_any_array_string_entry(pfx,
> + "SONET: OC-3, short reach");
> /* Ethernet Compliance Codes */
> if (id[6] & (1 << 7))
> - sprintf(value, "%s", "Ethernet: BASE-PX");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: BASE-PX");
> if (id[6] & (1 << 6))
> - sprintf(value, "%s", "Ethernet: BASE-BX10");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: BASE-BX10");
> if (id[6] & (1 << 5))
> - sprintf(value, "%s", "Ethernet: 100BASE-FX");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: 100BASE-FX");
> if (id[6] & (1 << 4))
> - sprintf(value, "%s", "Ethernet: 100BASE-LX/LX10");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: 100BASE-LX/LX10");
> if (id[6] & (1 << 3))
> - sprintf(value, "%s", "Ethernet: 1000BASE-T");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: 1000BASE-T");
> if (id[6] & (1 << 2))
> - sprintf(value, "%s", "Ethernet: 1000BASE-CX");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: 1000BASE-CX");
> if (id[6] & (1 << 1))
> - sprintf(value, "%s", "Ethernet: 1000BASE-LX");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: 1000BASE-LX");
> if (id[6] & (1 << 0))
> - sprintf(value, "%s", "Ethernet: 1000BASE-SX");
> + module_print_any_array_string_entry(pfx,
> + "Ethernet: 1000BASE-SX");
> /* Fibre Channel link length */
> if (id[7] & (1 << 7))
> - sprintf(value, "%s", "FC: very long distance (V)");
> + module_print_any_array_string_entry(pfx,
> + "FC: very long distance (V)");
> if (id[7] & (1 << 6))
> - sprintf(value, "%s", "FC: short distance (S)");
> + module_print_any_array_string_entry(pfx,
> + "FC: short distance (S)");
> if (id[7] & (1 << 5))
> - sprintf(value, "%s", "FC: intermediate distance (I)");
> + module_print_any_array_string_entry(pfx,
> + "FC: intermediate distance (I)");
> if (id[7] & (1 << 4))
> - sprintf(value, "%s", "FC: long distance (L)");
> + module_print_any_array_string_entry(pfx,
> + "FC: long distance (L)");
> if (id[7] & (1 << 3))
> - sprintf(value, "%s", "FC: medium distance (M)");
> + module_print_any_array_string_entry(pfx,
> + "FC: medium distance (M)");
> /* Fibre Channel transmitter technology */
> if (id[7] & (1 << 2))
> - sprintf(value, "%s", "FC: Shortwave laser, linear Rx (SA)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Shortwave laser, linear Rx (SA)");
> if (id[7] & (1 << 1))
> - sprintf(value, "%s", "FC: Longwave laser (LC)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Longwave laser (LC)");
> if (id[7] & (1 << 0))
> - sprintf(value, "%s", "FC: Electrical inter-enclosure (EL)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Electrical inter-enclosure (EL)");
> if (id[8] & (1 << 7))
> - sprintf(value, "%s", "FC: Electrical intra-enclosure (EL)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Electrical intra-enclosure (EL)");
> if (id[8] & (1 << 6))
> - sprintf(value, "%s", "FC: Shortwave laser w/o OFC (SN)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Shortwave laser w/o OFC (SN)");
> if (id[8] & (1 << 5))
> - sprintf(value, "%s", "FC: Shortwave laser with OFC (SL)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Shortwave laser with OFC (SL)");
> if (id[8] & (1 << 4))
> - sprintf(value, "%s", "FC: Longwave laser (LL)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Longwave laser (LL)");
> if (id[8] & (1 << 3))
> - sprintf(value, "%s", "Active Cable");
> + module_print_any_array_string_entry(pfx,
> + "Active Cable");
> if (id[8] & (1 << 2))
> - sprintf(value, "%s", "Passive Cable");
> + module_print_any_array_string_entry(pfx,
> + "Passive Cable");
> if (id[8] & (1 << 1))
> - sprintf(value, "%s", "FC: Copper FC-BaseT");
> + module_print_any_array_string_entry(pfx,
> + "FC: Copper FC-BaseT");
> /* Fibre Channel transmission media */
> if (id[9] & (1 << 7))
> - sprintf(value, "%s", "FC: Twin Axial Pair (TW)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Twin Axial Pair (TW)");
> if (id[9] & (1 << 6))
> - sprintf(value, "%s", "FC: Twisted Pair (TP)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Twisted Pair (TP)");
> if (id[9] & (1 << 5))
> - sprintf(value, "%s", "FC: Miniature Coax (MI)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Miniature Coax (MI)");
> if (id[9] & (1 << 4))
> - sprintf(value, "%s", "FC: Video Coax (TV)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Video Coax (TV)");
> if (id[9] & (1 << 3))
> - sprintf(value, "%s", "FC: Multimode, 62.5um (M6)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Multimode, 62.5um (M6)");
> if (id[9] & (1 << 2))
> - sprintf(value, "%s", "FC: Multimode, 50um (M5)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Multimode, 50um (M5)");
> if (id[9] & (1 << 0))
> - sprintf(value, "%s", "FC: Single Mode (SM)");
> + module_print_any_array_string_entry(pfx,
> + "FC: Single Mode (SM)");
> /* Fibre Channel speed */
> if (id[10] & (1 << 7))
> - sprintf(value, "%s", "FC: 1200 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 1200 MBytes/sec");
> if (id[10] & (1 << 6))
> - sprintf(value, "%s", "FC: 800 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 800 MBytes/sec");
> if (id[10] & (1 << 5))
> - sprintf(value, "%s", "FC: 1600 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 1600 MBytes/sec");
> if (id[10] & (1 << 4))
> - sprintf(value, "%s", "FC: 400 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 400 MBytes/sec");
> if (id[10] & (1 << 3))
> - sprintf(value, "%s", "FC: 3200 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 3200 MBytes/sec");
> if (id[10] & (1 << 2))
> - sprintf(value, "%s", "FC: 200 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 200 MBytes/sec");
> if (id[10] & (1 << 0))
> - sprintf(value, "%s", "FC: 100 MBytes/sec");
> + module_print_any_array_string_entry(pfx,
> + "FC: 100 MBytes/sec");
> /* Extended Specification Compliance Codes from SFF-8024 */
> if (id[36] == 0x1)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G AOC or 25GAUI C2M AOC with
> worst BER of 5x10^(-5)");
> if (id[36] == 0x2)
> - sprintf(value, "%s", "Extended: 100G Base-SR4 or 25GBase-
> SR");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 100G Base-SR4 or 25GBase-SR");
> if (id[36] == 0x3)
> - sprintf(value, "%s", "Extended: 100G Base-LR4 or 25GBase-
> LR");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 100G Base-LR4 or 25GBase-LR");
> if (id[36] == 0x4)
> - sprintf(value, "%s", "Extended: 100G Base-ER4 or 25GBase-
> ER");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 100G Base-ER4 or 25GBase-ER");
> if (id[36] == 0x8)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G ACC or 25GAUI C2M ACC with worst
> BER of 5x10^(-5)");
> if (id[36] == 0xb)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G Base-CR4 or 25G Base-CR CA-L");
> if (id[36] == 0xc)
> - sprintf(value, "%s", "Extended: 25G Base-CR CA-S");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 25G Base-CR CA-S");
> if (id[36] == 0xd)
> - sprintf(value, "%s", "Extended: 25G Base-CR CA-N");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 25G Base-CR CA-N");
> if (id[36] == 0x16)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 10Gbase-T with SFI electrical interface");
> if (id[36] == 0x18)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G AOC or 25GAUI C2M AOC with
> worst BER of 10^(-12)");
> if (id[36] == 0x19)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G ACC or 25GAUI C2M ACC with worst
> BER of 10^(-12)");
> if (id[36] == 0x1a)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100GE-DWDM2 (DWDM transceiver using
> 2 wavelengths on a 1550 nm DWDM grid with a reach up to 80 km)");
> if (id[36] == 0x1b)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G 1550nm WDM (4 wavelengths)");
> if (id[36] == 0x1c)
> - sprintf(value, "%s", "Extended: 10Gbase-T Short Reach");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 10Gbase-T Short Reach");
> if (id[36] == 0x1d)
> - sprintf(value, "%s", "Extended: 5GBASE-T");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 5GBASE-T");
> if (id[36] == 0x1e)
> - sprintf(value, "%s", "Extended: 2.5GBASE-T");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 2.5GBASE-T");
> if (id[36] == 0x1f)
> - sprintf(value, "%s", "Extended: 40G SWDM4");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 40G SWDM4");
> if (id[36] == 0x20)
> - sprintf(value, "%s", "Extended: 100G SWDM4");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 100G SWDM4");
> if (id[36] == 0x21)
> - sprintf(value, "%s", "Extended: 100G PAM4 BiDi");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 100G PAM4 BiDi");
> if (id[36] == 0x22)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 4WDM-10 MSA (10km version of 100G
> CWDM4 with same RS(528,514) FEC in host system)");
> if (id[36] == 0x23)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 4WDM-20 MSA (20km version of
> 100GBASE-LR4 with RS(528,514) FEC in host system)");
> if (id[36] == 0x24)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 4WDM-40 MSA (40km reach with APD
> receiver and RS(528,514) FEC in host system)");
> if (id[36] == 0x25)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100GBASE-DR (clause 140), CAUI-4 (no
> FEC)");
> if (id[36] == 0x26)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G-FR or 100GBASE-FR1 (clause 140),
> CAUI-4 (no FEC)");
> if (id[36] == 0x27)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 100G-LR or 100GBASE-LR1 (clause 140),
> CAUI-4 (no FEC)");
> if (id[36] == 0x30)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: Active Copper Cable with 50GAUI,
> 100GAUI-2 or 200GAUI-4 C2M. Providing a worst BER of 10-6 or below");
> if (id[36] == 0x31)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: Active Optical Cable with 50GAUI,
> 100GAUI-2 or 200GAUI-4 C2M. Providing a worst BER of 10-6 or below");
> if (id[36] == 0x32)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: Active Copper Cable with 50GAUI,
> 100GAUI-2 or 200GAUI-4 C2M. Providing a worst BER of 2.6x10-4 for ACC,
> 10-5 for AUI, or below");
> if (id[36] == 0x33)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: Active Optical Cable with 50GAUI,
> 100GAUI-2 or 200GAUI-4 C2M. Providing a worst BER of 2.6x10-4 for ACC,
> 10-5 for AUI, or below");
> if (id[36] == 0x40)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 50GBASE-CR, 100GBASE-CR2, or
> 200GBASE-CR4");
> if (id[36] == 0x41)
> - sprintf(value, "%s",
> + module_print_any_array_string_entry(pfx,
> "Extended: 50GBASE-SR, 100GBASE-SR2, or
> 200GBASE-SR4");
> if (id[36] == 0x42)
> - sprintf(value, "%s", "Extended: 50GBASE-FR or 200GBASE-
> DR4");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 50GBASE-FR or 200GBASE-DR4");
> if (id[36] == 0x43)
> - sprintf(value, "%s", "Extended: 200GBASE-FR4");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 200GBASE-FR4");
> if (id[36] == 0x44)
> - sprintf(value, "%s", "Extended: 200G 1550 nm PSM4");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 200G 1550 nm PSM4");
> if (id[36] == 0x45)
> - sprintf(value, "%s", "Extended: 50GBASE-LR");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 50GBASE-LR");
> if (id[36] == 0x46)
> - sprintf(value, "%s", "Extended: 200GBASE-LR4");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 200GBASE-LR4");
> if (id[36] == 0x50)
> - sprintf(value, "%s", "Extended: 64GFC EA");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 64GFC EA");
> if (id[36] == 0x51)
> - sprintf(value, "%s", "Extended: 64GFC SW");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 64GFC SW");
> if (id[36] == 0x52)
> - sprintf(value, "%s", "Extended: 64GFC LW");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 64GFC LW");
> if (id[36] == 0x53)
> - sprintf(value, "%s", "Extended: 128GFC EA");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 128GFC EA");
> if (id[36] == 0x54)
> - sprintf(value, "%s", "Extended: 128GFC SW");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 128GFC SW");
> if (id[36] == 0x55)
> - sprintf(value, "%s", "Extended: 128GFC LW");
> + module_print_any_array_string_entry(pfx,
> + "Extended: 128GFC LW");
>
> - if (value[0] != '\0')
> - module_print_any_string(pfx, value);
> + if (is_json_context())
> + close_json_array("");
> }
>
> static void sff8079_show_encoding(const __u8 *id)
> --
> 2.53.0
^ permalink raw reply
* RE: [PATCH ethtool-next v2 1/2] sfpid: print all implemented options
From: Danielle Ratson @ 2026-07-12 18:15 UTC (permalink / raw)
To: Aleksander Jan Bajkowski, mkubecek@suse.cz, andrew@lunn.ch,
davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, jbe@pengutronix.de, netdev@vger.kernel.org
In-Reply-To: <20260711095803.257213-1-olek2@wp.pl>
> -----Original Message-----
> From: Aleksander Jan Bajkowski <olek2@wp.pl>
> Sent: Saturday, 11 July 2026 12:54
> To: Danielle Ratson <danieller@nvidia.com>; mkubecek@suse.cz;
> andrew@lunn.ch; davem@davemloft.net; edumazet@google.com;
> kuba@kernel.org; pabeni@redhat.com; jbe@pengutronix.de;
> netdev@vger.kernel.org
> Cc: Aleksander Jan Bajkowski <olek2@wp.pl>
> Subject: [PATCH ethtool-next v2 1/2] sfpid: print all implemented options
>
> SFP modules implement multiple options. Before the “json” option was
> introduced, all options were listed. Currently, only the last option is listed. This
> commit fixes this bug. Options are represented as array.
>
> Before:
> $ ethtool -m sfp-wan
> ...
> Option values : 0x00 0x32
> Option : RATE_SELECT implemented
> ...
> $ ethtool --json -m sfp-wan
> [ {
> ...
> "option_values": [ 0,50 ],
> "option": "RATE_SELECT implemented", ...
> } ]
>
> After:
> $ ethtool -m sfp-wan
> ...
> Option values : 0x00 0x32
> Option : RX_LOS implemented
> Option : TX_DISABLE implemented
> Option : RATE_SELECT implemented
> ...
> $ ethtool --json -m sfp-wan
> [ {
> ...
> "option_values": [ 0,50 ],
> "option": [ "RX_LOS implemented","TX_DISABLE
> implemented","RATE_SELECT implemented" ], ...
> } ]
>
> Fixes: 4071862f58d8 ("sfpid: Add JSON output handling to --module-info in
> SFF8079 modules")
> Signed-off-by: Aleksander Jan Bajkowski <olek2@wp.pl>
> ---
> Changes in v2:
> - fix typo introduced -> introduced
> - renamr module_print_array_string() ->
> module_print_any_array_string_entry()
> ---
> module-common.c | 8 ++++++++
> module-common.h | 1 +
> sfpid.c | 48 ++++++++++++++++++++++++++++++++----------------
> 3 files changed, 41 insertions(+), 16 deletions(-)
>
> diff --git a/module-common.c b/module-common.c index 42fccf6..43ff649
> 100644
> --- a/module-common.c
> +++ b/module-common.c
> @@ -258,6 +258,14 @@ void module_print_any_bool(const char *fn, char
> *given_json_fn, bool value,
> printf("\t%-41s : %s\n", fn, str_value); }
>
> +void module_print_any_array_string_entry(const char *fn, const char
> +*value) {
> + if (is_json_context())
> + print_string(PRINT_JSON, NULL, "%s", value);
> + else
> + printf("\t%-41s : %s\n", fn, value);
> +}
> +
> void module_show_value_with_unit(const __u8 *id, unsigned int reg,
> const char *name, unsigned int mult,
> const char *unit)
> diff --git a/module-common.h b/module-common.h index 4063448..f3baf2a
> 100644
> --- a/module-common.h
> +++ b/module-common.h
> @@ -281,6 +281,7 @@ void module_print_any_string(const char *fn, const
> char *value); void module_print_any_float(const char *fn, float value, const
> char *unit); void module_print_any_bool(const char *fn, char *given_json_fn,
> bool value,
> const char *str_value);
> +void module_print_any_array_string_entry(const char *fn, const char
> +*value);
> void module_show_value_with_unit(const __u8 *id, unsigned int reg,
> const char *name, unsigned int mult,
> const char *unit);
> diff --git a/sfpid.c b/sfpid.c
> index 74a6f51..ec5dd95 100644
> --- a/sfpid.c
> +++ b/sfpid.c
> @@ -396,7 +396,6 @@ static void
> sff8079_show_wavelength_or_copper_compliance(const __u8 *id) static void
> sff8079_show_options(const __u8 *id) {
> static const char *pfx = "Option";
> - char value[64] = "";
>
> if (is_json_context()) {
> open_json_array("option_values", ""); @@ -407,35 +406,52
> @@ static void sff8079_show_options(const __u8 *id)
> printf("\t%-41s : 0x%02x 0x%02x\n", "Option values", id[64],
> id[65]);
> }
> +
> + if (is_json_context())
> + open_json_array("option", "");
> +
> if (id[65] & (1 << 1))
> - sprintf(value, "%s", "RX_LOS implemented");
> + module_print_any_array_string_entry(pfx,
> + "RX_LOS implemented");
Indentation, here and a lot more similar places on both patches, is wrong and needs to be aligned to the open paren. Checkpatch flags that, please run it on the patchset.
Also, like in this case and also some other places are unnecessarily wrapped, even though they fit in 80 cols.
> if (id[65] & (1 << 2))
> - sprintf(value, "%s", "RX_LOS implemented, inverted");
> + module_print_any_array_string_entry(pfx,
> + "RX_LOS implemented, inverted");
> if (id[65] & (1 << 3))
> - sprintf(value, "%s", "TX_FAULT implemented");
> + module_print_any_array_string_entry(pfx,
> + "TX_FAULT implemented");
> if (id[65] & (1 << 4))
> - sprintf(value, "%s", "TX_DISABLE implemented");
> + module_print_any_array_string_entry(pfx,
> + "TX_DISABLE implemented");
> if (id[65] & (1 << 5))
> - sprintf(value, "%s", "RATE_SELECT implemented");
> + module_print_any_array_string_entry(pfx,
> + "RATE_SELECT implemented");
> if (id[65] & (1 << 6))
> - sprintf(value, "%s", "Tunable transmitter technology");
> + module_print_any_array_string_entry(pfx,
> + "Tunable transmitter technology");
> if (id[65] & (1 << 7))
> - sprintf(value, "%s", "Receiver decision threshold
> implemented");
> + module_print_any_array_string_entry(pfx,
> + "Receiver decision threshold implemented");
> if (id[64] & (1 << 0))
> - sprintf(value, "%s", "Linear receiver output implemented");
> + module_print_any_array_string_entry(pfx,
> + "Linear receiver output implemented");
> if (id[64] & (1 << 1))
> - sprintf(value, "%s", "Power level 2 requirement");
> + module_print_any_array_string_entry(pfx,
> + "Power level 2 requirement");
> if (id[64] & (1 << 2))
> - sprintf(value, "%s", "Cooled transceiver implemented");
> + module_print_any_array_string_entry(pfx,
> + "Cooled transceiver implemented");
> if (id[64] & (1 << 3))
> - sprintf(value, "%s", "Retimer or CDR implemented");
> + module_print_any_array_string_entry(pfx,
> + "Retimer or CDR implemented");
> if (id[64] & (1 << 4))
> - sprintf(value, "%s", "Paging implemented");
> + module_print_any_array_string_entry(pfx,
> + "Paging implemented");
> if (id[64] & (1 << 5))
> - sprintf(value, "%s", "Power level 3 requirement");
> + module_print_any_array_string_entry(pfx,
> + "Power level 3 requirement");
>
> - if (value[0] != '\0')
> - module_print_any_string(pfx, value);
> + if (is_json_context())
> + close_json_array("");
> }
>
> static void sff8079_show_all_common(const __u8 *id)
> --
> 2.53.0
^ permalink raw reply
* [PATCH net] esp: do not unref managed frag pages in esp_ssg_unref()
From: Maher Azzouzi @ 2026-07-12 17:05 UTC (permalink / raw)
To: steffen.klassert, herbert, davem
Cc: netdev, edumazet, kuba, pabeni, w, linux-kernel, Maher Azzouzi
esp_ssg_unref() releases the page references held on the source
scatterlist after the AEAD operation completes. It calls
skb_page_unref() on every frag page for an out-of-place transform
(req->src != req->dst), and in the error path of esp_output_tail()
(already_unref == true) on the request's own scatterlist.
This is wrong when the skb carries managed frags
(SKBFL_MANAGED_FRAG_REFS). Managed frags are owned by a zerocopy ubuf
and the skb does not hold a per-frag page reference; io_uring SEND_ZC
with a registered buffer attaches the bvec pages this way via
io_sg_from_iter(). The rest of the stack honours this invariant:
skb_release_data() skips the per-frag unref when SKBFL_MANAGED_FRAG_REFS
is set, and skb_zcopy_managed() is the guard used at the other unref
sites.
esp_ssg_unref() is missing that guard, so for a managed-frag skb it
drops a page reference the skb never acquired. This can underflow the
page reference count and free a page that is still in use.
Guard the function with skb_zcopy_managed() so both unref paths are
skipped for managed-frag skbs, matching skb_release_data().
Fixes: cac2661c53f3 ("esp4: Avoid skb_cow_data whenever possible")
Fixes: 03e2a30f6a27 ("esp6: Avoid skb_cow_data whenever possible")
Signed-off-by: Maher Azzouzi <maherazz04@gmail.com>
---
net/ipv4/esp4.c | 7 +++++++
net/ipv6/esp6.c | 7 +++++++
2 files changed, 14 insertions(+)
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
index dfc81ee..fa1710e 100644
--- a/net/ipv4/esp4.c
+++ b/net/ipv4/esp4.c
@@ -104,6 +104,13 @@ static void esp_ssg_unref(struct xfrm_state *x, void *tmp, struct sk_buff *skb,
struct aead_request *req;
struct scatterlist *sg;
+ /* Managed frags are owned by the zerocopy ubuf; the skb holds no
+ * per-frag page reference, so we must not drop one here. Mirrors
+ * the SKBFL_MANAGED_FRAG_REFS handling in skb_release_data().
+ */
+ if (skb_zcopy_managed(skb))
+ return;
+
if (x->props.flags & XFRM_STATE_ESN)
extralen += sizeof(struct esp_output_extra);
diff --git a/net/ipv6/esp6.c b/net/ipv6/esp6.c
index 296b579..7d216b9 100644
--- a/net/ipv6/esp6.c
+++ b/net/ipv6/esp6.c
@@ -121,6 +121,13 @@ static void esp_ssg_unref(struct xfrm_state *x, void *tmp, struct sk_buff *skb,
struct aead_request *req;
struct scatterlist *sg;
+ /* Managed frags are owned by the zerocopy ubuf; the skb holds no
+ * per-frag page reference, so we must not drop one here. Mirrors
+ * the SKBFL_MANAGED_FRAG_REFS handling in skb_release_data().
+ */
+ if (skb_zcopy_managed(skb))
+ return;
+
if (x->props.flags & XFRM_STATE_ESN)
extralen += sizeof(struct esp_output_extra);
--
2.34.1
^ permalink raw reply related
* Re: [PATCH 11/13 RFC net-next] net: cipso: guard IPv4 packet manipulation functions
From: Paul Moore @ 2026-07-12 16:22 UTC (permalink / raw)
To: Fernando Fernandez Mancera
Cc: netdev, davem, edumazet, kuba, pabeni, dsahern, horms, idosch,
linux-security-module, linux-kernel
In-Reply-To: <20260712013941.4570-12-fmancera@suse.de>
On Sat, Jul 11, 2026 at 9:41 PM Fernando Fernandez Mancera
<fmancera@suse.de> wrote:
>
> To enable compiling the network stack without IPv4, the CIPSO functions
> that manipulate IPv4 options and generate ICMP errors must be bypassed.
>
> Ideally, CIPSO should not be compiled when IPv4 is disabled but
> currently it is too integrated within netlabel, so let's just bypassed
> the relevant functions.
>
> Signed-off-by: Fernando Fernandez Mancera <fmancera@suse.de>
> ---
> net/ipv4/cipso_ipv4.c | 10 ++++++++++
> 1 file changed, 10 insertions(+)
I think I would prefer to make CONFIG_NETLABEL dependent on
CONFIG_IPV4 at this point in time. This will keep the code cleaner
and allow time to do the proper work of wrapping the CIPSO code with
CONFIG_CIPSO (or similar) and making that dependent on CONFIG_IPV4.
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH net] nfc: llcp: reject PDUs shorter than the LLCP header
From: Doruk (0sec) @ 2026-07-12 16:02 UTC (permalink / raw)
To: vadim.fedorenko; +Cc: david, oe-linux-nfc, netdev, linux-kernel, stable
In-Reply-To: <be1731eb-e6ec-4015-92e4-c09fd88019e6@linux.dev>
Hi Vadim
this was reproduced from userspace on unmodified
linux-next (bee763d5f341) without RF hardware.
It's the peer-RX path, not a local command skb:
virtual_ncidev_write (peer NCI DATA) -> nci_rx_data_packet
-> nfc_tm_data_received -> nfc_llcp_data_received
-> rx_work -> nfc_llcp_rx_skb -> nfc_llcp_recv_connect
Bring the LLCP link up via a normal NFC-DEP activation, then send
one NCI DATA packet with a 1-byte CONNECT PDU. skb->len - 2 wraps
to 0xffffffff and the TLV walk runs off the end:
BUG: KFENCE: out-of-bounds read in nfc_llcp_recv_connect+0x9f6/0xf80
nfc_llcp_recv_connect+0x9f6 -> nfc_llcp_rx_work -> process_one_work
read 4219B past a 704B skbuff_small_head from virtual_ncidev_write
R14: 00000000ffffffff (wrapped tlv_array_len)
With the guard: rx_skb runs for all 600 short PDUs, recv_connect
reached 0 times, 0 reports.
The bound stays "<", not "<=" -- a header-only SYMM/DISC/DM is
exactly 2 bytes and must still dispatch; AGF uses "<=" only
because an AGF frame must also carry a sub-PDU. I'll drop the
"same guard as AGF" line from the commit message.
Instantiating /dev/virtual_nci needs privilege, but that's just
the syzbot transport; the 1-byte CONNECT is what a remote NFC-DEP
peer emits, and the DEP layer imposes no minimum LLCP length.
Impact is a proximity OOB read (DoS).
I can send the full reproducer if you'd like.
best
Doruk
On Sun, Jul 12, 2026 02:01 PM, Vadim Fedorenko
<vadim.fedorenko@linux.dev> wrote:
>
> On 11/07/2026 08:27, Doruk Tan Ozturk wrote:
> > nfc_llcp_rx_skb() reads the two-byte LLCP header (DSAP/SSAP/PTYPE) and
> > dispatches by PDU type; several handlers then derive a TLV-array length as
> > skb->len - LLCP_HEADER_SIZE. Neither nfc_llcp_rx_skb() nor its callers
> > guarantee the frame is at least LLCP_HEADER_SIZE bytes, and a sub-header
>
> that's not correct. there are 2 ways to get to nfc_llcp_rx_skb() - via
> nfc_llcp_recv_agf() or through commands/locally generated skbs. The
> first one checks against LLCP_HEADER_SIZE, while latter one creates skb
> payload with correct LLCP header size. Do you have a reproducer to
> trigger the issue?
>
>
> > PDU does reach it: digital_in_recv_dep_res() and digital_tg_recv_dep_req()
> > strip the DEP header with skb_pull() after only checking the DEP header
> > size, so a DEP I-PDU carrying a 0- or 1-byte LLCP payload is handed up as
> > a sub-2-byte skb.
> >
> > For a CONNECT or CC PDU, nfc_llcp_recv_connect() and nfc_llcp_recv_cc()
> > then pass skb->len - LLCP_HEADER_SIZE to nfc_llcp_parse_connection_tlv().
> > For skb->len < 2 that subtraction underflows: truncated into the u16
> > tlv_array_len parameter it becomes ~0xFFFE, and for a CONNECT to the SDP
> > SAP, nfc_llcp_connect_sn() uses a size_t and underflows to SIZE_MAX. The
> > TLV parsers bound their walk relative to that length, so they read far
> > past the end of the skb.
> >
> > The aggregated-frame path (nfc_llcp_recv_agf()) already drops sub-PDUs
> > shorter than the header. Apply the same guard once, in the dispatcher, so
>
> that not exactly correct, it drops skbs which are shorter or equal to
> the header, the check added in this patch is not correct then.
>
> > every PDU type is covered.
> >
> > Found by 0sec (https://0sec.ai) using automated source analysis; the
> > missing guard is evident from source. Compile-tested.
> >
> > Fixes: d646960f7986 ("NFC: Initial LLCP support")
> > Cc: stable@vger.kernel.org
> > Assisted-by: 0sec:claude-opus-4-8
> > Signed-off-by: Doruk Tan Ozturk <doruk@0sec.ai>
> > ---
> > net/nfc/llcp_core.c | 3 +++
> > 1 file changed, 3 insertions(+)
> >
> > diff --git a/net/nfc/llcp_core.c b/net/nfc/llcp_core.c
> > index aed5fe1afef0..e3b3077e0e83 100644
> > --- a/net/nfc/llcp_core.c
> > +++ b/net/nfc/llcp_core.c
> > @@ -1481,6 +1481,9 @@ static void nfc_llcp_rx_skb(struct nfc_llcp_local *local, struct sk_buff *skb)
> > {
> > u8 dsap, ssap, ptype;
> >
> > + if (skb->len < LLCP_HEADER_SIZE)
> > + return;
> > +
> > ptype = nfc_llcp_ptype(skb);
> > dsap = nfc_llcp_dsap(skb);
> > ssap = nfc_llcp_ssap(skb);
>
^ permalink raw reply
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