* [PATCH net-next 02/11] ipvs: add ip_vs_status info
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
From: Julian Anastasov <ja@ssi.bg>
Add /proc/net/ip_vs_status to show current state of IPVS.
The motivation for this new /proc interface is to provide the output
for the users to help them decide when to tune the load factor for
hash tables, which is possible with the new sysctl knobs coming in
followup patch.
The output also includes information for the kthreads used for stats.
Signed-off-by: Julian Anastasov <ja@ssi.bg>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/ipvs/ip_vs_ctl.c | 145 +++++++++++++++++++++++++++++++++
1 file changed, 145 insertions(+)
diff --git a/net/netfilter/ipvs/ip_vs_ctl.c b/net/netfilter/ipvs/ip_vs_ctl.c
index 1322dd54ed7c..fb1df61edfdd 100644
--- a/net/netfilter/ipvs/ip_vs_ctl.c
+++ b/net/netfilter/ipvs/ip_vs_ctl.c
@@ -2924,6 +2924,144 @@ static int ip_vs_stats_percpu_show(struct seq_file *seq, void *v)
return 0;
}
+
+static int ip_vs_status_show(struct seq_file *seq, void *v)
+{
+ struct net *net = seq_file_single_net(seq);
+ struct netns_ipvs *ipvs = net_ipvs(net);
+ unsigned int resched_score = 0;
+ struct ip_vs_conn_hnode *hn;
+ struct hlist_bl_head *head;
+ struct ip_vs_service *svc;
+ struct ip_vs_rht *t, *pt;
+ struct hlist_bl_node *e;
+ int old_gen, new_gen;
+ u32 counts[8];
+ u32 bucket;
+ int count;
+ u32 sum1;
+ u32 sum;
+ int i;
+
+ rcu_read_lock();
+
+ t = rcu_dereference(ipvs->conn_tab);
+
+ seq_printf(seq, "Conns:\t%d\n", atomic_read(&ipvs->conn_count));
+ seq_printf(seq, "Conn buckets:\t%d (%d bits, lfactor %d)\n",
+ t ? t->size : 0, t ? t->bits : 0, t ? t->lfactor : 0);
+
+ if (!atomic_read(&ipvs->conn_count))
+ goto after_conns;
+ old_gen = atomic_read(&ipvs->conn_tab_changes);
+
+repeat_conn:
+ smp_rmb(); /* ipvs->conn_tab and conn_tab_changes */
+ memset(counts, 0, sizeof(counts));
+ ip_vs_rht_for_each_table_rcu(ipvs->conn_tab, t, pt) {
+ for (bucket = 0; bucket < t->size; bucket++) {
+ DECLARE_IP_VS_RHT_WALK_BUCKET_RCU();
+
+ count = 0;
+ resched_score++;
+ ip_vs_rht_walk_bucket_rcu(t, bucket, head) {
+ count = 0;
+ hlist_bl_for_each_entry_rcu(hn, e, head, node)
+ count++;
+ }
+ resched_score += count;
+ if (resched_score >= 100) {
+ resched_score = 0;
+ cond_resched_rcu();
+ new_gen = atomic_read(&ipvs->conn_tab_changes);
+ /* New table installed ? */
+ if (old_gen != new_gen) {
+ old_gen = new_gen;
+ goto repeat_conn;
+ }
+ }
+ counts[min(count, (int)ARRAY_SIZE(counts) - 1)]++;
+ }
+ }
+ for (sum = 0, i = 0; i < ARRAY_SIZE(counts); i++)
+ sum += counts[i];
+ sum1 = sum - counts[0];
+ seq_printf(seq, "Conn buckets empty:\t%u (%lu%%)\n",
+ counts[0], (unsigned long)counts[0] * 100 / max(sum, 1U));
+ for (i = 1; i < ARRAY_SIZE(counts); i++) {
+ if (!counts[i])
+ continue;
+ seq_printf(seq, "Conn buckets len-%d:\t%u (%lu%%)\n",
+ i, counts[i],
+ (unsigned long)counts[i] * 100 / max(sum1, 1U));
+ }
+
+after_conns:
+ t = rcu_dereference(ipvs->svc_table);
+
+ count = ip_vs_get_num_services(ipvs);
+ seq_printf(seq, "Services:\t%d\n", count);
+ seq_printf(seq, "Service buckets:\t%d (%d bits, lfactor %d)\n",
+ t ? t->size : 0, t ? t->bits : 0, t ? t->lfactor : 0);
+
+ if (!count)
+ goto after_svc;
+ old_gen = atomic_read(&ipvs->svc_table_changes);
+
+repeat_svc:
+ smp_rmb(); /* ipvs->svc_table and svc_table_changes */
+ memset(counts, 0, sizeof(counts));
+ ip_vs_rht_for_each_table_rcu(ipvs->svc_table, t, pt) {
+ for (bucket = 0; bucket < t->size; bucket++) {
+ DECLARE_IP_VS_RHT_WALK_BUCKET_RCU();
+
+ count = 0;
+ resched_score++;
+ ip_vs_rht_walk_bucket_rcu(t, bucket, head) {
+ count = 0;
+ hlist_bl_for_each_entry_rcu(svc, e, head,
+ s_list)
+ count++;
+ }
+ resched_score += count;
+ if (resched_score >= 100) {
+ resched_score = 0;
+ cond_resched_rcu();
+ new_gen = atomic_read(&ipvs->svc_table_changes);
+ /* New table installed ? */
+ if (old_gen != new_gen) {
+ old_gen = new_gen;
+ goto repeat_svc;
+ }
+ }
+ counts[min(count, (int)ARRAY_SIZE(counts) - 1)]++;
+ }
+ }
+ for (sum = 0, i = 0; i < ARRAY_SIZE(counts); i++)
+ sum += counts[i];
+ sum1 = sum - counts[0];
+ seq_printf(seq, "Service buckets empty:\t%u (%lu%%)\n",
+ counts[0], (unsigned long)counts[0] * 100 / max(sum, 1U));
+ for (i = 1; i < ARRAY_SIZE(counts); i++) {
+ if (!counts[i])
+ continue;
+ seq_printf(seq, "Service buckets len-%d:\t%u (%lu%%)\n",
+ i, counts[i],
+ (unsigned long)counts[i] * 100 / max(sum1, 1U));
+ }
+
+after_svc:
+ seq_printf(seq, "Stats thread slots:\t%d (max %lu)\n",
+ ipvs->est_kt_count, ipvs->est_max_threads);
+ seq_printf(seq, "Stats chain max len:\t%d\n", ipvs->est_chain_max);
+ seq_printf(seq, "Stats thread ests:\t%d\n",
+ ipvs->est_chain_max * IPVS_EST_CHAIN_FACTOR *
+ IPVS_EST_NTICKS);
+
+ rcu_read_unlock();
+ return 0;
+}
+
#endif
/*
@@ -4825,6 +4963,9 @@ int __net_init ip_vs_control_net_init(struct netns_ipvs *ipvs)
ipvs->net->proc_net,
ip_vs_stats_percpu_show, NULL))
goto err_percpu;
+ if (!proc_create_net_single("ip_vs_status", 0, ipvs->net->proc_net,
+ ip_vs_status_show, NULL))
+ goto err_status;
#endif
ret = ip_vs_control_net_init_sysctl(ipvs);
@@ -4835,6 +4976,9 @@ int __net_init ip_vs_control_net_init(struct netns_ipvs *ipvs)
err:
#ifdef CONFIG_PROC_FS
+ remove_proc_entry("ip_vs_status", ipvs->net->proc_net);
+
+err_status:
remove_proc_entry("ip_vs_stats_percpu", ipvs->net->proc_net);
err_percpu:
@@ -4860,6 +5004,7 @@ void __net_exit ip_vs_control_net_cleanup(struct netns_ipvs *ipvs)
ip_vs_control_net_cleanup_sysctl(ipvs);
cancel_delayed_work_sync(&ipvs->est_reload_work);
#ifdef CONFIG_PROC_FS
+ remove_proc_entry("ip_vs_status", ipvs->net->proc_net);
remove_proc_entry("ip_vs_stats_percpu", ipvs->net->proc_net);
remove_proc_entry("ip_vs_stats", ipvs->net->proc_net);
remove_proc_entry("ip_vs", ipvs->net->proc_net);
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 03/11] ipvs: add conn_lfactor and svc_lfactor sysctl vars
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
From: Julian Anastasov <ja@ssi.bg>
Allow the default load factor for the connection and service tables
to be configured.
Signed-off-by: Julian Anastasov <ja@ssi.bg>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
Documentation/networking/ipvs-sysctl.rst | 37 ++++++++++++
net/netfilter/ipvs/ip_vs_ctl.c | 76 ++++++++++++++++++++++++
2 files changed, 113 insertions(+)
diff --git a/Documentation/networking/ipvs-sysctl.rst b/Documentation/networking/ipvs-sysctl.rst
index 3fb5fa142eef..a556439f8be7 100644
--- a/Documentation/networking/ipvs-sysctl.rst
+++ b/Documentation/networking/ipvs-sysctl.rst
@@ -29,6 +29,33 @@ backup_only - BOOLEAN
If set, disable the director function while the server is
in backup mode to avoid packet loops for DR/TUN methods.
+conn_lfactor - INTEGER
+ Possible values: -8 (larger table) .. 8 (smaller table)
+
+ Default: -4
+
+ Controls the sizing of the connection hash table based on the
+ load factor (number of connections per table buckets):
+
+ 2^conn_lfactor = nodes / buckets
+
+ As result, the table grows if load increases and shrinks when
+ load decreases in the range of 2^8 - 2^conn_tab_bits (module
+ parameter).
+ The value is a shift count where negative values select
+ buckets = (connection hash nodes << -value) while positive
+ values select buckets = (connection hash nodes >> value). The
+ negative values reduce the collisions and reduce the time for
+ lookups but increase the table size. Positive values will
+ tolerate load above 100% when using smaller table is
+ preferred with the cost of more collisions. If using NAT
+ connections consider decreasing the value with one because
+ they add two nodes in the hash table.
+
+ Example:
+ -4: grow if load goes above 6% (buckets = nodes * 16)
+ 2: grow if load goes above 400% (buckets = nodes / 4)
+
conn_reuse_mode - INTEGER
1 - default
@@ -219,6 +246,16 @@ secure_tcp - INTEGER
The value definition is the same as that of drop_entry and
drop_packet.
+svc_lfactor - INTEGER
+ Possible values: -8 (larger table) .. 8 (smaller table)
+
+ Default: -3
+
+ Controls the sizing of the service hash table based on the
+ load factor (number of services per table buckets). The table
+ will grow and shrink in the range of 2^4 - 2^20.
+ See conn_lfactor for explanation.
+
sync_threshold - vector of 2 INTEGERs: sync_threshold, sync_period
default 3 50
diff --git a/net/netfilter/ipvs/ip_vs_ctl.c b/net/netfilter/ipvs/ip_vs_ctl.c
index fb1df61edfdd..6632daa87ded 100644
--- a/net/netfilter/ipvs/ip_vs_ctl.c
+++ b/net/netfilter/ipvs/ip_vs_ctl.c
@@ -2445,6 +2445,60 @@ static int ipvs_proc_run_estimation(const struct ctl_table *table, int write,
return ret;
}
+static int ipvs_proc_conn_lfactor(const struct ctl_table *table, int write,
+ void *buffer, size_t *lenp, loff_t *ppos)
+{
+ struct netns_ipvs *ipvs = table->extra2;
+ int *valp = table->data;
+ int val = *valp;
+ int ret;
+
+ struct ctl_table tmp_table = {
+ .data = &val,
+ .maxlen = sizeof(int),
+ };
+
+ ret = proc_dointvec(&tmp_table, write, buffer, lenp, ppos);
+ if (write && ret >= 0) {
+ if (val < -8 || val > 8) {
+ ret = -EINVAL;
+ } else {
+ *valp = val;
+ if (rcu_access_pointer(ipvs->conn_tab))
+ mod_delayed_work(system_unbound_wq,
+ &ipvs->conn_resize_work, 0);
+ }
+ }
+ return ret;
+}
+
+static int ipvs_proc_svc_lfactor(const struct ctl_table *table, int write,
+ void *buffer, size_t *lenp, loff_t *ppos)
+{
+ struct netns_ipvs *ipvs = table->extra2;
+ int *valp = table->data;
+ int val = *valp;
+ int ret;
+
+ struct ctl_table tmp_table = {
+ .data = &val,
+ .maxlen = sizeof(int),
+ };
+
+ ret = proc_dointvec(&tmp_table, write, buffer, lenp, ppos);
+ if (write && ret >= 0) {
+ if (val < -8 || val > 8) {
+ ret = -EINVAL;
+ } else {
+ *valp = val;
+ if (rcu_access_pointer(ipvs->svc_table))
+ mod_delayed_work(system_unbound_wq,
+ &ipvs->svc_resize_work, 0);
+ }
+ }
+ return ret;
+}
+
/*
* IPVS sysctl table (under the /proc/sys/net/ipv4/vs/)
* Do not change order or insert new entries without
@@ -2633,6 +2687,18 @@ static struct ctl_table vs_vars[] = {
.mode = 0644,
.proc_handler = ipvs_proc_est_nice,
},
+ {
+ .procname = "conn_lfactor",
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = ipvs_proc_conn_lfactor,
+ },
+ {
+ .procname = "svc_lfactor",
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = ipvs_proc_svc_lfactor,
+ },
#ifdef CONFIG_IP_VS_DEBUG
{
.procname = "debug_level",
@@ -4853,6 +4919,16 @@ static int __net_init ip_vs_control_net_init_sysctl(struct netns_ipvs *ipvs)
tbl[idx].extra2 = ipvs;
tbl[idx++].data = &ipvs->sysctl_est_nice;
+ if (unpriv)
+ tbl[idx].mode = 0444;
+ tbl[idx].extra2 = ipvs;
+ tbl[idx++].data = &ipvs->sysctl_conn_lfactor;
+
+ if (unpriv)
+ tbl[idx].mode = 0444;
+ tbl[idx].extra2 = ipvs;
+ tbl[idx++].data = &ipvs->sysctl_svc_lfactor;
+
#ifdef CONFIG_IP_VS_DEBUG
/* Global sysctls must be ro in non-init netns */
if (!net_eq(net, &init_net))
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 04/11] netfilter: x_physdev: reject empty or not-nul terminated device names
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
Reject names that lack a \0 character and reject the empty string as
well. iptables allows this but it fails to re-parse iptables-save output
that contain such rules.
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/xt_physdev.c | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/net/netfilter/xt_physdev.c b/net/netfilter/xt_physdev.c
index 343e65f377d4..53997771013f 100644
--- a/net/netfilter/xt_physdev.c
+++ b/net/netfilter/xt_physdev.c
@@ -107,6 +107,28 @@ static int physdev_mt_check(const struct xt_mtchk_param *par)
return -EINVAL;
}
+#define X(memb) strnlen(info->memb, sizeof(info->memb)) >= sizeof(info->memb)
+ if (info->bitmask & XT_PHYSDEV_OP_IN) {
+ if (info->physindev[0] == '\0')
+ return -EINVAL;
+ if (X(physindev))
+ return -ENAMETOOLONG;
+ }
+
+ if (info->bitmask & XT_PHYSDEV_OP_OUT) {
+ if (info->physoutdev[0] == '\0')
+ return -EINVAL;
+
+ if (X(physoutdev))
+ return -ENAMETOOLONG;
+ }
+
+ if (X(in_mask))
+ return -ENAMETOOLONG;
+ if (X(out_mask))
+ return -ENAMETOOLONG;
+#undef X
+
if (!brnf_probed) {
brnf_probed = true;
request_module("br_netfilter");
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 05/11] netfilter: nfnetlink: prefer skb_mac_header helpers
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
This adds implicit DEBUG_WARN_ON_ONCE for debug configurations.
No other changes intended.
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/nfnetlink_log.c | 19 ++++++++++---------
net/netfilter/nfnetlink_queue.c | 25 ++++++++++++-------------
2 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/net/netfilter/nfnetlink_log.c b/net/netfilter/nfnetlink_log.c
index b2c24cb919d4..2439cbbd5b26 100644
--- a/net/netfilter/nfnetlink_log.c
+++ b/net/netfilter/nfnetlink_log.c
@@ -401,7 +401,7 @@ nfulnl_timer(struct timer_list *t)
static u32 nfulnl_get_bridge_size(const struct sk_buff *skb)
{
- u32 size = 0;
+ u32 mac_len, size = 0;
if (!skb_mac_header_was_set(skb))
return 0;
@@ -412,14 +412,17 @@ static u32 nfulnl_get_bridge_size(const struct sk_buff *skb)
size += nla_total_size(sizeof(u16)); /* tag */
}
- if (skb->network_header > skb->mac_header)
- size += nla_total_size(skb->network_header - skb->mac_header);
+ mac_len = skb_mac_header_len(skb);
+ if (mac_len > 0)
+ size += nla_total_size(mac_len);
return size;
}
static int nfulnl_put_bridge(struct nfulnl_instance *inst, const struct sk_buff *skb)
{
+ u32 mac_len;
+
if (!skb_mac_header_was_set(skb))
return 0;
@@ -437,12 +440,10 @@ static int nfulnl_put_bridge(struct nfulnl_instance *inst, const struct sk_buff
nla_nest_end(inst->skb, nest);
}
- if (skb->mac_header < skb->network_header) {
- int len = (int)(skb->network_header - skb->mac_header);
-
- if (nla_put(inst->skb, NFULA_L2HDR, len, skb_mac_header(skb)))
- goto nla_put_failure;
- }
+ mac_len = skb_mac_header_len(skb);
+ if (mac_len > 0 &&
+ nla_put(inst->skb, NFULA_L2HDR, mac_len, skb_mac_header(skb)))
+ goto nla_put_failure;
return 0;
diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
index c7ee6f6ff725..58304fd1f70f 100644
--- a/net/netfilter/nfnetlink_queue.c
+++ b/net/netfilter/nfnetlink_queue.c
@@ -579,6 +579,7 @@ static u32 nfqnl_get_bridge_size(struct nf_queue_entry *entry)
{
struct sk_buff *entskb = entry->skb;
u32 nlalen = 0;
+ u32 mac_len;
if (entry->state.pf != PF_BRIDGE || !skb_mac_header_was_set(entskb))
return 0;
@@ -587,9 +588,9 @@ static u32 nfqnl_get_bridge_size(struct nf_queue_entry *entry)
nlalen += nla_total_size(nla_total_size(sizeof(__be16)) +
nla_total_size(sizeof(__be16)));
- if (entskb->network_header > entskb->mac_header)
- nlalen += nla_total_size((entskb->network_header -
- entskb->mac_header));
+ mac_len = skb_mac_header_len(entskb);
+ if (mac_len > 0)
+ nlalen += nla_total_size(mac_len);
return nlalen;
}
@@ -597,6 +598,7 @@ static u32 nfqnl_get_bridge_size(struct nf_queue_entry *entry)
static int nfqnl_put_bridge(struct nf_queue_entry *entry, struct sk_buff *skb)
{
struct sk_buff *entskb = entry->skb;
+ u32 mac_len;
if (entry->state.pf != PF_BRIDGE || !skb_mac_header_was_set(entskb))
return 0;
@@ -615,12 +617,10 @@ static int nfqnl_put_bridge(struct nf_queue_entry *entry, struct sk_buff *skb)
nla_nest_end(skb, nest);
}
- if (entskb->mac_header < entskb->network_header) {
- int len = (int)(entskb->network_header - entskb->mac_header);
-
- if (nla_put(skb, NFQA_L2HDR, len, skb_mac_header(entskb)))
- goto nla_put_failure;
- }
+ mac_len = skb_mac_header_len(entskb);
+ if (mac_len > 0 &&
+ nla_put(skb, NFQA_L2HDR, mac_len, skb_mac_header(entskb)))
+ goto nla_put_failure;
return 0;
@@ -1004,13 +1004,13 @@ nf_queue_entry_dup(struct nf_queue_entry *e)
static void nf_bridge_adjust_skb_data(struct sk_buff *skb)
{
if (nf_bridge_info_get(skb))
- __skb_push(skb, skb->network_header - skb->mac_header);
+ __skb_push(skb, skb_mac_header_len(skb));
}
static void nf_bridge_adjust_segmented_data(struct sk_buff *skb)
{
if (nf_bridge_info_get(skb))
- __skb_pull(skb, skb->network_header - skb->mac_header);
+ __skb_pull(skb, skb_mac_header_len(skb));
}
#else
#define nf_bridge_adjust_skb_data(s) do {} while (0)
@@ -1469,8 +1469,7 @@ static int nfqa_parse_bridge(struct nf_queue_entry *entry,
}
if (nfqa[NFQA_L2HDR]) {
- int mac_header_len = entry->skb->network_header -
- entry->skb->mac_header;
+ u32 mac_header_len = skb_mac_header_len(entry->skb);
if (mac_header_len != nla_len(nfqa[NFQA_L2HDR]))
return -EINVAL;
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 06/11] netfilter: xt_HL: add pr_fmt and checkentry validation
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
From: Marino Dzalto <marino.dzalto@gmail.com>
Add pr_fmt to prefix log messages with the module name for
easier debugging in dmesg.
Add checkentry functions for IPv4 (ttl_mt_check) and IPv6
(hl_mt6_check) to validate the match mode at rule registration
time, rejecting invalid modes with -EINVAL.
The evaluation function returns false in case the mode is
unknown, so this is a cleanup, not a bug fix.
Signed-off-by: Marino Dzalto <marino.dzalto@gmail.com>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/xt_hl.c | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/net/netfilter/xt_hl.c b/net/netfilter/xt_hl.c
index c1a70f8f0441..4a12a757ecbf 100644
--- a/net/netfilter/xt_hl.c
+++ b/net/netfilter/xt_hl.c
@@ -6,6 +6,7 @@
* Hop Limit matching module
* (C) 2001-2002 Maciej Soltysiak <solt@dns.toxicfilms.tv>
*/
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/ip.h>
#include <linux/ipv6.h>
@@ -22,6 +23,18 @@ MODULE_LICENSE("GPL");
MODULE_ALIAS("ipt_ttl");
MODULE_ALIAS("ip6t_hl");
+static int ttl_mt_check(const struct xt_mtchk_param *par)
+{
+ const struct ipt_ttl_info *info = par->matchinfo;
+
+ if (info->mode > IPT_TTL_GT) {
+ pr_err("Unknown TTL match mode: %d\n", info->mode);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
static bool ttl_mt(const struct sk_buff *skb, struct xt_action_param *par)
{
const struct ipt_ttl_info *info = par->matchinfo;
@@ -41,6 +54,18 @@ static bool ttl_mt(const struct sk_buff *skb, struct xt_action_param *par)
return false;
}
+static int hl_mt6_check(const struct xt_mtchk_param *par)
+{
+ const struct ip6t_hl_info *info = par->matchinfo;
+
+ if (info->mode > IP6T_HL_GT) {
+ pr_err("Unknown Hop Limit match mode: %d\n", info->mode);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
static bool hl_mt6(const struct sk_buff *skb, struct xt_action_param *par)
{
const struct ip6t_hl_info *info = par->matchinfo;
@@ -65,6 +90,7 @@ static struct xt_match hl_mt_reg[] __read_mostly = {
.name = "ttl",
.revision = 0,
.family = NFPROTO_IPV4,
+ .checkentry = ttl_mt_check,
.match = ttl_mt,
.matchsize = sizeof(struct ipt_ttl_info),
.me = THIS_MODULE,
@@ -73,6 +99,7 @@ static struct xt_match hl_mt_reg[] __read_mostly = {
.name = "hl",
.revision = 0,
.family = NFPROTO_IPV6,
+ .checkentry = hl_mt6_check,
.match = hl_mt6,
.matchsize = sizeof(struct ip6t_hl_info),
.me = THIS_MODULE,
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 07/11] netfilter: xt_socket: enable defrag after all other checks
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
Originally this did not matter because defrag was enabled once per netns
and only disabled again on netns dismantle. When this got changed I should
have adjusted checkentry to not leave defrag enabled on error.
Fixes: de8c12110a13 ("netfilter: disable defrag once its no longer needed")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/xt_socket.c | 23 ++++++-----------------
1 file changed, 6 insertions(+), 17 deletions(-)
diff --git a/net/netfilter/xt_socket.c b/net/netfilter/xt_socket.c
index 76e01f292aaf..811e53bee408 100644
--- a/net/netfilter/xt_socket.c
+++ b/net/netfilter/xt_socket.c
@@ -168,52 +168,41 @@ static int socket_mt_enable_defrag(struct net *net, int family)
static int socket_mt_v1_check(const struct xt_mtchk_param *par)
{
const struct xt_socket_mtinfo1 *info = (struct xt_socket_mtinfo1 *) par->matchinfo;
- int err;
-
- err = socket_mt_enable_defrag(par->net, par->family);
- if (err)
- return err;
if (info->flags & ~XT_SOCKET_FLAGS_V1) {
pr_info_ratelimited("unknown flags 0x%x\n",
info->flags & ~XT_SOCKET_FLAGS_V1);
return -EINVAL;
}
- return 0;
+
+ return socket_mt_enable_defrag(par->net, par->family);
}
static int socket_mt_v2_check(const struct xt_mtchk_param *par)
{
const struct xt_socket_mtinfo2 *info = (struct xt_socket_mtinfo2 *) par->matchinfo;
- int err;
-
- err = socket_mt_enable_defrag(par->net, par->family);
- if (err)
- return err;
if (info->flags & ~XT_SOCKET_FLAGS_V2) {
pr_info_ratelimited("unknown flags 0x%x\n",
info->flags & ~XT_SOCKET_FLAGS_V2);
return -EINVAL;
}
- return 0;
+
+ return socket_mt_enable_defrag(par->net, par->family);
}
static int socket_mt_v3_check(const struct xt_mtchk_param *par)
{
const struct xt_socket_mtinfo3 *info =
(struct xt_socket_mtinfo3 *)par->matchinfo;
- int err;
- err = socket_mt_enable_defrag(par->net, par->family);
- if (err)
- return err;
if (info->flags & ~XT_SOCKET_FLAGS_V3) {
pr_info_ratelimited("unknown flags 0x%x\n",
info->flags & ~XT_SOCKET_FLAGS_V3);
return -EINVAL;
}
- return 0;
+
+ return socket_mt_enable_defrag(par->net, par->family);
}
static void socket_mt_destroy(const struct xt_mtdtor_param *par)
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 08/11] netfilter: conntrack: remove UDP-Lite conntrack support
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
From: Fernando Fernandez Mancera <fmancera@suse.de>
UDP-Lite (RFC 3828) socket support was recently retired from the core
networking stack. As a follow-up of that, drop the connection tracker
and NAT support for UDP-Lite in Netfilter.
This patch removes CONFIG_NF_CT_PROTO_UDPLITE and scrubs UDP-Lite
awareness from the conntrack core, NAT core, nft_ct, and ctnetlink.
Please note that stateless packet inspection, matching, ipsets or
logging support for IPPROTO_UDPLITE is preserved.
As conntrack no longer extracts UDP-Lite ports or tracks its L4 state,
when performing NAT the UDP-Lite checksum cannot be updated anymore.
That is an expected and acceptable consequence of removing UDP-Lite
conntrack module.
Signed-off-by: Fernando Fernandez Mancera <fmancera@suse.de>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
.../net/netfilter/ipv4/nf_conntrack_ipv4.h | 3 -
include/net/netfilter/nf_conntrack_l4proto.h | 7 --
net/netfilter/Kconfig | 11 --
net/netfilter/nf_conntrack_core.c | 8 --
net/netfilter/nf_conntrack_proto.c | 3 -
net/netfilter/nf_conntrack_proto_udp.c | 108 ------------------
net/netfilter/nf_conntrack_standalone.c | 2 -
net/netfilter/nf_nat_core.c | 6 -
net/netfilter/nf_nat_proto.c | 20 ----
net/netfilter/nfnetlink_cttimeout.c | 1 -
net/netfilter/nft_ct.c | 1 -
11 files changed, 170 deletions(-)
diff --git a/include/net/netfilter/ipv4/nf_conntrack_ipv4.h b/include/net/netfilter/ipv4/nf_conntrack_ipv4.h
index 8d65ffbf57de..b39417ad955e 100644
--- a/include/net/netfilter/ipv4/nf_conntrack_ipv4.h
+++ b/include/net/netfilter/ipv4/nf_conntrack_ipv4.h
@@ -16,9 +16,6 @@ extern const struct nf_conntrack_l4proto nf_conntrack_l4proto_icmp;
#ifdef CONFIG_NF_CT_PROTO_SCTP
extern const struct nf_conntrack_l4proto nf_conntrack_l4proto_sctp;
#endif
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
-extern const struct nf_conntrack_l4proto nf_conntrack_l4proto_udplite;
-#endif
#ifdef CONFIG_NF_CT_PROTO_GRE
extern const struct nf_conntrack_l4proto nf_conntrack_l4proto_gre;
#endif
diff --git a/include/net/netfilter/nf_conntrack_l4proto.h b/include/net/netfilter/nf_conntrack_l4proto.h
index cd5020835a6d..fde2427ceb8f 100644
--- a/include/net/netfilter/nf_conntrack_l4proto.h
+++ b/include/net/netfilter/nf_conntrack_l4proto.h
@@ -107,11 +107,6 @@ int nf_conntrack_udp_packet(struct nf_conn *ct,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
const struct nf_hook_state *state);
-int nf_conntrack_udplite_packet(struct nf_conn *ct,
- struct sk_buff *skb,
- unsigned int dataoff,
- enum ip_conntrack_info ctinfo,
- const struct nf_hook_state *state);
int nf_conntrack_tcp_packet(struct nf_conn *ct,
struct sk_buff *skb,
unsigned int dataoff,
@@ -139,8 +134,6 @@ void nf_conntrack_icmpv6_init_net(struct net *net);
/* Existing built-in generic protocol */
extern const struct nf_conntrack_l4proto nf_conntrack_l4proto_generic;
-#define MAX_NF_CT_PROTO IPPROTO_UDPLITE
-
const struct nf_conntrack_l4proto *nf_ct_l4proto_find(u8 l4proto);
/* Generic netlink helpers */
diff --git a/net/netfilter/Kconfig b/net/netfilter/Kconfig
index f3ea0cb26f36..682c675125fc 100644
--- a/net/netfilter/Kconfig
+++ b/net/netfilter/Kconfig
@@ -209,17 +209,6 @@ config NF_CT_PROTO_SCTP
If unsure, say Y.
-config NF_CT_PROTO_UDPLITE
- bool 'UDP-Lite protocol connection tracking support'
- depends on NETFILTER_ADVANCED
- default y
- help
- With this option enabled, the layer 3 independent connection
- tracking code will be able to do state tracking on UDP-Lite
- connections.
-
- If unsure, say Y.
-
config NF_CONNTRACK_AMANDA
tristate "Amanda backup protocol support"
depends on NETFILTER_ADVANCED
diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c
index 27ce5fda8993..b08189226320 100644
--- a/net/netfilter/nf_conntrack_core.c
+++ b/net/netfilter/nf_conntrack_core.c
@@ -323,9 +323,6 @@ nf_ct_get_tuple(const struct sk_buff *skb,
#endif
case IPPROTO_TCP:
case IPPROTO_UDP:
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
- case IPPROTO_UDPLITE:
-#endif
#ifdef CONFIG_NF_CT_PROTO_SCTP
case IPPROTO_SCTP:
#endif
@@ -1987,11 +1984,6 @@ static int nf_conntrack_handle_packet(struct nf_conn *ct,
case IPPROTO_ICMPV6:
return nf_conntrack_icmpv6_packet(ct, skb, ctinfo, state);
#endif
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
- case IPPROTO_UDPLITE:
- return nf_conntrack_udplite_packet(ct, skb, dataoff,
- ctinfo, state);
-#endif
#ifdef CONFIG_NF_CT_PROTO_SCTP
case IPPROTO_SCTP:
return nf_conntrack_sctp_packet(ct, skb, dataoff,
diff --git a/net/netfilter/nf_conntrack_proto.c b/net/netfilter/nf_conntrack_proto.c
index bc1d96686b9c..50ddd3d613e1 100644
--- a/net/netfilter/nf_conntrack_proto.c
+++ b/net/netfilter/nf_conntrack_proto.c
@@ -103,9 +103,6 @@ const struct nf_conntrack_l4proto *nf_ct_l4proto_find(u8 l4proto)
#ifdef CONFIG_NF_CT_PROTO_SCTP
case IPPROTO_SCTP: return &nf_conntrack_l4proto_sctp;
#endif
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
- case IPPROTO_UDPLITE: return &nf_conntrack_l4proto_udplite;
-#endif
#ifdef CONFIG_NF_CT_PROTO_GRE
case IPPROTO_GRE: return &nf_conntrack_l4proto_gre;
#endif
diff --git a/net/netfilter/nf_conntrack_proto_udp.c b/net/netfilter/nf_conntrack_proto_udp.c
index 0030fbe8885c..cc9b7e5e1935 100644
--- a/net/netfilter/nf_conntrack_proto_udp.c
+++ b/net/netfilter/nf_conntrack_proto_udp.c
@@ -129,91 +129,6 @@ int nf_conntrack_udp_packet(struct nf_conn *ct,
return NF_ACCEPT;
}
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
-static void udplite_error_log(const struct sk_buff *skb,
- const struct nf_hook_state *state,
- const char *msg)
-{
- nf_l4proto_log_invalid(skb, state, IPPROTO_UDPLITE, "%s", msg);
-}
-
-static bool udplite_error(struct sk_buff *skb,
- unsigned int dataoff,
- const struct nf_hook_state *state)
-{
- unsigned int udplen = skb->len - dataoff;
- const struct udphdr *hdr;
- struct udphdr _hdr;
- unsigned int cscov;
-
- /* Header is too small? */
- hdr = skb_header_pointer(skb, dataoff, sizeof(_hdr), &_hdr);
- if (!hdr) {
- udplite_error_log(skb, state, "short packet");
- return true;
- }
-
- cscov = ntohs(hdr->len);
- if (cscov == 0) {
- cscov = udplen;
- } else if (cscov < sizeof(*hdr) || cscov > udplen) {
- udplite_error_log(skb, state, "invalid checksum coverage");
- return true;
- }
-
- /* UDPLITE mandates checksums */
- if (!hdr->check) {
- udplite_error_log(skb, state, "checksum missing");
- return true;
- }
-
- /* Checksum invalid? Ignore. */
- if (state->hook == NF_INET_PRE_ROUTING &&
- state->net->ct.sysctl_checksum &&
- nf_checksum_partial(skb, state->hook, dataoff, cscov, IPPROTO_UDP,
- state->pf)) {
- udplite_error_log(skb, state, "bad checksum");
- return true;
- }
-
- return false;
-}
-
-/* Returns verdict for packet, and may modify conntracktype */
-int nf_conntrack_udplite_packet(struct nf_conn *ct,
- struct sk_buff *skb,
- unsigned int dataoff,
- enum ip_conntrack_info ctinfo,
- const struct nf_hook_state *state)
-{
- unsigned int *timeouts;
-
- if (udplite_error(skb, dataoff, state))
- return -NF_ACCEPT;
-
- timeouts = nf_ct_timeout_lookup(ct);
- if (!timeouts)
- timeouts = udp_get_timeouts(nf_ct_net(ct));
-
- /* If we've seen traffic both ways, this is some kind of UDP
- stream. Extend timeout. */
- if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
- nf_ct_refresh_acct(ct, ctinfo, skb,
- timeouts[UDP_CT_REPLIED]);
-
- if (unlikely((ct->status & IPS_NAT_CLASH)))
- return NF_ACCEPT;
-
- /* Also, more likely to be important, and not a probe */
- if (!test_and_set_bit(IPS_ASSURED_BIT, &ct->status))
- nf_conntrack_event_cache(IPCT_ASSURED, ct);
- } else {
- nf_ct_refresh_acct(ct, ctinfo, skb, timeouts[UDP_CT_UNREPLIED]);
- }
- return NF_ACCEPT;
-}
-#endif
-
#ifdef CONFIG_NF_CONNTRACK_TIMEOUT
#include <linux/netfilter/nfnetlink.h>
@@ -299,26 +214,3 @@ const struct nf_conntrack_l4proto nf_conntrack_l4proto_udp =
},
#endif /* CONFIG_NF_CONNTRACK_TIMEOUT */
};
-
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
-const struct nf_conntrack_l4proto nf_conntrack_l4proto_udplite =
-{
- .l4proto = IPPROTO_UDPLITE,
- .allow_clash = true,
-#if IS_ENABLED(CONFIG_NF_CT_NETLINK)
- .tuple_to_nlattr = nf_ct_port_tuple_to_nlattr,
- .nlattr_to_tuple = nf_ct_port_nlattr_to_tuple,
- .nlattr_tuple_size = nf_ct_port_nlattr_tuple_size,
- .nla_policy = nf_ct_port_nla_policy,
-#endif
-#ifdef CONFIG_NF_CONNTRACK_TIMEOUT
- .ctnl_timeout = {
- .nlattr_to_obj = udp_timeout_nlattr_to_obj,
- .obj_to_nlattr = udp_timeout_obj_to_nlattr,
- .nlattr_max = CTA_TIMEOUT_UDP_MAX,
- .obj_size = sizeof(unsigned int) * CTA_TIMEOUT_UDP_MAX,
- .nla_policy = udp_timeout_nla_policy,
- },
-#endif /* CONFIG_NF_CONNTRACK_TIMEOUT */
-};
-#endif
diff --git a/net/netfilter/nf_conntrack_standalone.c b/net/netfilter/nf_conntrack_standalone.c
index 207b240b14e5..be2953c7d702 100644
--- a/net/netfilter/nf_conntrack_standalone.c
+++ b/net/netfilter/nf_conntrack_standalone.c
@@ -61,7 +61,6 @@ print_tuple(struct seq_file *s, const struct nf_conntrack_tuple *tuple,
ntohs(tuple->src.u.tcp.port),
ntohs(tuple->dst.u.tcp.port));
break;
- case IPPROTO_UDPLITE:
case IPPROTO_UDP:
seq_printf(s, "sport=%hu dport=%hu ",
ntohs(tuple->src.u.udp.port),
@@ -277,7 +276,6 @@ static const char* l4proto_name(u16 proto)
case IPPROTO_UDP: return "udp";
case IPPROTO_GRE: return "gre";
case IPPROTO_SCTP: return "sctp";
- case IPPROTO_UDPLITE: return "udplite";
case IPPROTO_ICMPV6: return "icmpv6";
}
diff --git a/net/netfilter/nf_nat_core.c b/net/netfilter/nf_nat_core.c
index 3b5434e4ec9c..83b2b5e9759a 100644
--- a/net/netfilter/nf_nat_core.c
+++ b/net/netfilter/nf_nat_core.c
@@ -68,7 +68,6 @@ static void nf_nat_ipv4_decode_session(struct sk_buff *skb,
fl4->daddr = t->dst.u3.ip;
if (t->dst.protonum == IPPROTO_TCP ||
t->dst.protonum == IPPROTO_UDP ||
- t->dst.protonum == IPPROTO_UDPLITE ||
t->dst.protonum == IPPROTO_SCTP)
fl4->fl4_dport = t->dst.u.all;
}
@@ -79,7 +78,6 @@ static void nf_nat_ipv4_decode_session(struct sk_buff *skb,
fl4->saddr = t->src.u3.ip;
if (t->dst.protonum == IPPROTO_TCP ||
t->dst.protonum == IPPROTO_UDP ||
- t->dst.protonum == IPPROTO_UDPLITE ||
t->dst.protonum == IPPROTO_SCTP)
fl4->fl4_sport = t->src.u.all;
}
@@ -99,7 +97,6 @@ static void nf_nat_ipv6_decode_session(struct sk_buff *skb,
fl6->daddr = t->dst.u3.in6;
if (t->dst.protonum == IPPROTO_TCP ||
t->dst.protonum == IPPROTO_UDP ||
- t->dst.protonum == IPPROTO_UDPLITE ||
t->dst.protonum == IPPROTO_SCTP)
fl6->fl6_dport = t->dst.u.all;
}
@@ -110,7 +107,6 @@ static void nf_nat_ipv6_decode_session(struct sk_buff *skb,
fl6->saddr = t->src.u3.in6;
if (t->dst.protonum == IPPROTO_TCP ||
t->dst.protonum == IPPROTO_UDP ||
- t->dst.protonum == IPPROTO_UDPLITE ||
t->dst.protonum == IPPROTO_SCTP)
fl6->fl6_sport = t->src.u.all;
}
@@ -415,7 +411,6 @@ static bool l4proto_in_range(const struct nf_conntrack_tuple *tuple,
case IPPROTO_GRE: /* all fall though */
case IPPROTO_TCP:
case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
case IPPROTO_SCTP:
if (maniptype == NF_NAT_MANIP_SRC)
port = tuple->src.u.all;
@@ -612,7 +607,6 @@ static void nf_nat_l4proto_unique_tuple(struct nf_conntrack_tuple *tuple,
goto find_free_id;
#endif
case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
case IPPROTO_TCP:
case IPPROTO_SCTP:
if (maniptype == NF_NAT_MANIP_SRC)
diff --git a/net/netfilter/nf_nat_proto.c b/net/netfilter/nf_nat_proto.c
index 97c0f841fc96..07f51fe75fbe 100644
--- a/net/netfilter/nf_nat_proto.c
+++ b/net/netfilter/nf_nat_proto.c
@@ -79,23 +79,6 @@ static bool udp_manip_pkt(struct sk_buff *skb,
return true;
}
-static bool udplite_manip_pkt(struct sk_buff *skb,
- unsigned int iphdroff, unsigned int hdroff,
- const struct nf_conntrack_tuple *tuple,
- enum nf_nat_manip_type maniptype)
-{
-#ifdef CONFIG_NF_CT_PROTO_UDPLITE
- struct udphdr *hdr;
-
- if (skb_ensure_writable(skb, hdroff + sizeof(*hdr)))
- return false;
-
- hdr = (struct udphdr *)(skb->data + hdroff);
- __udp_manip_pkt(skb, iphdroff, hdr, tuple, maniptype, true);
-#endif
- return true;
-}
-
static bool
sctp_manip_pkt(struct sk_buff *skb,
unsigned int iphdroff, unsigned int hdroff,
@@ -287,9 +270,6 @@ static bool l4proto_manip_pkt(struct sk_buff *skb,
case IPPROTO_UDP:
return udp_manip_pkt(skb, iphdroff, hdroff,
tuple, maniptype);
- case IPPROTO_UDPLITE:
- return udplite_manip_pkt(skb, iphdroff, hdroff,
- tuple, maniptype);
case IPPROTO_SCTP:
return sctp_manip_pkt(skb, iphdroff, hdroff,
tuple, maniptype);
diff --git a/net/netfilter/nfnetlink_cttimeout.c b/net/netfilter/nfnetlink_cttimeout.c
index fd8652aa7e88..dca6826af7de 100644
--- a/net/netfilter/nfnetlink_cttimeout.c
+++ b/net/netfilter/nfnetlink_cttimeout.c
@@ -457,7 +457,6 @@ static int cttimeout_default_get(struct sk_buff *skb,
timeouts = nf_tcp_pernet(info->net)->timeouts;
break;
case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
timeouts = nf_udp_pernet(info->net)->timeouts;
break;
case IPPROTO_ICMPV6:
diff --git a/net/netfilter/nft_ct.c b/net/netfilter/nft_ct.c
index 425525b90ac9..60ee8d932fcb 100644
--- a/net/netfilter/nft_ct.c
+++ b/net/netfilter/nft_ct.c
@@ -1252,7 +1252,6 @@ static int nft_ct_expect_obj_init(const struct nft_ctx *ctx,
switch (priv->l4proto) {
case IPPROTO_TCP:
case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
case IPPROTO_DCCP:
case IPPROTO_SCTP:
break;
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 09/11] netfilter: x_tables: Avoid a couple -Wflex-array-member-not-at-end warnings
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
From: "Gustavo A. R. Silva" <gustavoars@kernel.org>
-Wflex-array-member-not-at-end was introduced in GCC-14, and we are
getting ready to enable it, globally.
Use the TRAILING_OVERLAP() helper to fix the following warnings:
1 net/netfilter/x_tables.c:816:39: warning: structure containing a flexible array member is not at the end of another structure [-Wflex-array-member-not-at-end]
1 net/netfilter/x_tables.c:811:39: warning: structure containing a flexible array member is not at the end of another structure [-Wflex-array-member-not-at-end]
This helper creates a union between a flexible-array member (FAM)
and a set of members that would otherwise follow it. This overlays
the trailing members onto the FAM while preserving the original
memory layout.
Signed-off-by: Gustavo A. R. Silva <gustavoars@kernel.org>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/x_tables.c | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/net/netfilter/x_tables.c b/net/netfilter/x_tables.c
index b39017c80548..9f837fb5ceb4 100644
--- a/net/netfilter/x_tables.c
+++ b/net/netfilter/x_tables.c
@@ -819,13 +819,17 @@ EXPORT_SYMBOL_GPL(xt_compat_match_to_user);
/* non-compat version may have padding after verdict */
struct compat_xt_standard_target {
- struct compat_xt_entry_target t;
- compat_uint_t verdict;
+ /* Must be last as it ends in a flexible-array member. */
+ TRAILING_OVERLAP(struct compat_xt_entry_target, t, data,
+ compat_uint_t verdict;
+ );
};
struct compat_xt_error_target {
- struct compat_xt_entry_target t;
- char errorname[XT_FUNCTION_MAXNAMELEN];
+ /* Must be last as it ends in a flexible-array member. */
+ TRAILING_OVERLAP(struct compat_xt_entry_target, t, data,
+ char errorname[XT_FUNCTION_MAXNAMELEN];
+ );
};
int xt_compat_check_entry_offsets(const void *base, const char *elems,
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 10/11] netfilter: nft_fwd_netdev: check ttl/hl before forwarding
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
Drop packets if their ttl/hl is too small for forwarding.
Fixes: d32de98ea70f ("netfilter: nft_fwd_netdev: allow to forward packets via neighbour layer")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/nft_fwd_netdev.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/net/netfilter/nft_fwd_netdev.c b/net/netfilter/nft_fwd_netdev.c
index ad48dcd45abe..4bce36c3a6a0 100644
--- a/net/netfilter/nft_fwd_netdev.c
+++ b/net/netfilter/nft_fwd_netdev.c
@@ -116,6 +116,11 @@ static void nft_fwd_neigh_eval(const struct nft_expr *expr,
goto out;
}
iph = ip_hdr(skb);
+ if (iph->ttl <= 1) {
+ verdict = NF_DROP;
+ goto out;
+ }
+
ip_decrease_ttl(iph);
neigh_table = NEIGH_ARP_TABLE;
break;
@@ -132,6 +137,11 @@ static void nft_fwd_neigh_eval(const struct nft_expr *expr,
goto out;
}
ip6h = ipv6_hdr(skb);
+ if (ip6h->hop_limit <= 1) {
+ verdict = NF_DROP;
+ goto out;
+ }
+
ip6h->hop_limit--;
neigh_table = NEIGH_ND_TABLE;
break;
--
2.52.0
^ permalink raw reply related
* [PATCH net-next 11/11] netfilter: require Ethernet MAC header before using eth_hdr()
From: Florian Westphal @ 2026-04-10 11:23 UTC (permalink / raw)
To: netdev
Cc: Paolo Abeni, David S. Miller, Eric Dumazet, Jakub Kicinski,
netfilter-devel, pablo
In-Reply-To: <20260410112352.23599-1-fw@strlen.de>
From: Zhengchuan Liang <zcliangcn@gmail.com>
`ip6t_eui64`, `xt_mac`, the `bitmap:ip,mac`, `hash:ip,mac`, and
`hash:mac` ipset types, and `nf_log_syslog` access `eth_hdr(skb)`
after either assuming that the skb is associated with an Ethernet
device or checking only that the `ETH_HLEN` bytes at
`skb_mac_header(skb)` lie between `skb->head` and `skb->data`.
Make these paths first verify that the skb is associated with an
Ethernet device, that the MAC header was set, and that it spans at
least a full Ethernet header before accessing `eth_hdr(skb)`.
Suggested-by: Florian Westphal <fw@strlen.de>
Tested-by: Ren Wei <enjou1224z@gmail.com>
Signed-off-by: Zhengchuan Liang <zcliangcn@gmail.com>
Signed-off-by: Ren Wei <n05ec@lzu.edu.cn>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/ipv6/netfilter/ip6t_eui64.c | 7 +++++--
net/netfilter/ipset/ip_set_bitmap_ipmac.c | 5 +++--
net/netfilter/ipset/ip_set_hash_ipmac.c | 9 +++++----
net/netfilter/ipset/ip_set_hash_mac.c | 5 +++--
net/netfilter/nf_log_syslog.c | 8 +++++++-
net/netfilter/xt_mac.c | 4 +---
6 files changed, 24 insertions(+), 14 deletions(-)
diff --git a/net/ipv6/netfilter/ip6t_eui64.c b/net/ipv6/netfilter/ip6t_eui64.c
index da69a27e8332..bbb684f9964c 100644
--- a/net/ipv6/netfilter/ip6t_eui64.c
+++ b/net/ipv6/netfilter/ip6t_eui64.c
@@ -7,6 +7,7 @@
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/ipv6.h>
+#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/netfilter/x_tables.h>
@@ -21,8 +22,10 @@ eui64_mt6(const struct sk_buff *skb, struct xt_action_param *par)
{
unsigned char eui64[8];
- if (!(skb_mac_header(skb) >= skb->head &&
- skb_mac_header(skb) + ETH_HLEN <= skb->data)) {
+ if (!skb->dev || skb->dev->type != ARPHRD_ETHER)
+ return false;
+
+ if (!skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN) {
par->hotdrop = true;
return false;
}
diff --git a/net/netfilter/ipset/ip_set_bitmap_ipmac.c b/net/netfilter/ipset/ip_set_bitmap_ipmac.c
index 2c625e0f49ec..752f59ef8744 100644
--- a/net/netfilter/ipset/ip_set_bitmap_ipmac.c
+++ b/net/netfilter/ipset/ip_set_bitmap_ipmac.c
@@ -11,6 +11,7 @@
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/errno.h>
+#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/netlink.h>
#include <linux/jiffies.h>
@@ -220,8 +221,8 @@ bitmap_ipmac_kadt(struct ip_set *set, const struct sk_buff *skb,
return -IPSET_ERR_BITMAP_RANGE;
/* Backward compatibility: we don't check the second flag */
- if (skb_mac_header(skb) < skb->head ||
- (skb_mac_header(skb) + ETH_HLEN) > skb->data)
+ if (!skb->dev || skb->dev->type != ARPHRD_ETHER ||
+ !skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN)
return -EINVAL;
e.id = ip_to_id(map, ip);
diff --git a/net/netfilter/ipset/ip_set_hash_ipmac.c b/net/netfilter/ipset/ip_set_hash_ipmac.c
index 467c59a83c0a..b9a2681e2488 100644
--- a/net/netfilter/ipset/ip_set_hash_ipmac.c
+++ b/net/netfilter/ipset/ip_set_hash_ipmac.c
@@ -11,6 +11,7 @@
#include <linux/skbuff.h>
#include <linux/errno.h>
#include <linux/random.h>
+#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <net/ip.h>
#include <net/ipv6.h>
@@ -89,8 +90,8 @@ hash_ipmac4_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_ipmac4_elem e = { .ip = 0, { .foo[0] = 0, .foo[1] = 0 } };
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- if (skb_mac_header(skb) < skb->head ||
- (skb_mac_header(skb) + ETH_HLEN) > skb->data)
+ if (!skb->dev || skb->dev->type != ARPHRD_ETHER ||
+ !skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN)
return -EINVAL;
if (opt->flags & IPSET_DIM_TWO_SRC)
@@ -205,8 +206,8 @@ hash_ipmac6_kadt(struct ip_set *set, const struct sk_buff *skb,
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- if (skb_mac_header(skb) < skb->head ||
- (skb_mac_header(skb) + ETH_HLEN) > skb->data)
+ if (!skb->dev || skb->dev->type != ARPHRD_ETHER ||
+ !skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN)
return -EINVAL;
if (opt->flags & IPSET_DIM_TWO_SRC)
diff --git a/net/netfilter/ipset/ip_set_hash_mac.c b/net/netfilter/ipset/ip_set_hash_mac.c
index 718814730acf..41a122591fe2 100644
--- a/net/netfilter/ipset/ip_set_hash_mac.c
+++ b/net/netfilter/ipset/ip_set_hash_mac.c
@@ -8,6 +8,7 @@
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/errno.h>
+#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <net/netlink.h>
@@ -77,8 +78,8 @@ hash_mac4_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_mac4_elem e = { { .foo[0] = 0, .foo[1] = 0 } };
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- if (skb_mac_header(skb) < skb->head ||
- (skb_mac_header(skb) + ETH_HLEN) > skb->data)
+ if (!skb->dev || skb->dev->type != ARPHRD_ETHER ||
+ !skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN)
return -EINVAL;
if (opt->flags & IPSET_DIM_ONE_SRC)
diff --git a/net/netfilter/nf_log_syslog.c b/net/netfilter/nf_log_syslog.c
index 0507d67cad27..7a8952b049d1 100644
--- a/net/netfilter/nf_log_syslog.c
+++ b/net/netfilter/nf_log_syslog.c
@@ -78,7 +78,10 @@ dump_arp_packet(struct nf_log_buf *m,
else
logflags = NF_LOG_DEFAULT_MASK;
- if (logflags & NF_LOG_MACDECODE) {
+ if ((logflags & NF_LOG_MACDECODE) &&
+ skb->dev && skb->dev->type == ARPHRD_ETHER &&
+ skb_mac_header_was_set(skb) &&
+ skb_mac_header_len(skb) >= ETH_HLEN) {
nf_log_buf_add(m, "MACSRC=%pM MACDST=%pM ",
eth_hdr(skb)->h_source, eth_hdr(skb)->h_dest);
nf_log_dump_vlan(m, skb);
@@ -797,6 +800,9 @@ static void dump_mac_header(struct nf_log_buf *m,
switch (dev->type) {
case ARPHRD_ETHER:
+ if (!skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN)
+ return;
+
nf_log_buf_add(m, "MACSRC=%pM MACDST=%pM ",
eth_hdr(skb)->h_source, eth_hdr(skb)->h_dest);
nf_log_dump_vlan(m, skb);
diff --git a/net/netfilter/xt_mac.c b/net/netfilter/xt_mac.c
index 81649da57ba5..4798cd2ca26e 100644
--- a/net/netfilter/xt_mac.c
+++ b/net/netfilter/xt_mac.c
@@ -29,9 +29,7 @@ static bool mac_mt(const struct sk_buff *skb, struct xt_action_param *par)
if (skb->dev == NULL || skb->dev->type != ARPHRD_ETHER)
return false;
- if (skb_mac_header(skb) < skb->head)
- return false;
- if (skb_mac_header(skb) + ETH_HLEN > skb->data)
+ if (!skb_mac_header_was_set(skb) || skb_mac_header_len(skb) < ETH_HLEN)
return false;
ret = ether_addr_equal(eth_hdr(skb)->h_source, info->srcaddr);
ret ^= info->invert;
--
2.52.0
^ permalink raw reply related
* Re: [PATCH net-next v3 04/12] net: airoha: Rely on net_device pointer in HTB callbacks
From: Benjamin Larsson @ 2026-04-10 11:29 UTC (permalink / raw)
To: Jakub Kicinski, lorenzo
Cc: andrew+netdev, davem, edumazet, pabeni, robh, krzk+dt, conor+dt,
ansuelsmth, linux-arm-kernel, linux-mediatek, netdev, devicetree
In-Reply-To: <20260410025722.1809973-1-kuba@kernel.org>
On 10/04/2026 04:57, Jakub Kicinski wrote:
> This is an AI-generated review of your patch. The human sending this
> email says "For your consideration."
> ---
> net: airoha: Introduce WAN device flag
>
> This commit introduces a WAN flag to specify whether a device is used to
> transmit or receive WAN or LAN traffic. It enables specifying multiple LAN
> devices but limits the configuration to a single WAN device.
>
[...]
>> + fallthrough;
>> + }
>> + case AIROHA_GDM2_IDX:
>> + /* GDM2 is always used as wan */
>> + dev->flags |= PRIV_FLAG_WAN;
>> break;
>> default:
>> break;
>> }
>>
>> - for (i = 0; i < eth->soc->num_ppe; i++)
>> - airoha_ppe_set_cpu_port(dev, i, airoha_get_fe_port(dev));
>> + airoha_dev_set_qdma(dev);
>> + airoha_set_macaddr(dev, netdev->dev_addr);
>
> Since the commit message notes it is possible to specify multiple LAN
> devices, does unconditionally calling airoha_set_macaddr() for each
> device overwrite the single shared hardware LAN MAC register?
>
> If so, could this cause only the last initialized LAN device to have its MAC
> correctly programmed into the hardware, leading to dropped unicast
> packets for the other LAN interfaces?
>
>> +
>> + if (!airoha_is_lan_gdm_dev(dev) &&
>> + (port->id == AIROHA_GDM3_IDX || port->id == AIROHA_GDM4_IDX)) {
>> + int err;
>
> [ ... ]
Hi, the frame engine can be configured with a range of wan mac addresses
and a range of lan mac addresses via registers that set the top 24 bits
and then 2 registers that form a range of the low 24 bits.
Documentation says that packets that fall into this mac address range
either on the lan side or the wan side are to be treated as layer 3
packages and if a packet is not then it will be handled as a layer 2 packet.
The exact implication of this and if it actually matters is unknown. But
traffic that comes in on an interface that is not matched by an
acceleration flow is usually forwarded to the cpu for further processing.
MvH
Benjamin Larsson
^ permalink raw reply
* Re: [PATCH net v4 5/8] net/sched: netem: batch-transfer ready packets to avoid child re-entrancy
From: Simon Horman @ 2026-04-10 11:36 UTC (permalink / raw)
To: stephen
Cc: Simon Horman, ncardwell, open list, francois.michel, davem,
netdev, pabeni, ysseung, edumazet, jhs, kuba, posk, jiri
In-Reply-To: <20260406172627.210894-6-stephen@networkplumber.org>
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
For local reproduction steps see https://netdev-ai.bots.linux.dev/ai-local.html
---
net/sched: netem: batch-transfer ready packets to avoid child re-entrancy
netem_dequeue_child() previously transferred one packet from the tfifo
to the child qdisc per dequeue call. Parents like HFSC that track
class active/inactive state on qlen transitions could see an enqueue
during dequeue, causing double-insertion into the eltree
(CVE-2025-37890, CVE-2025-38001). Non-work-conserving children like
TBF could also refuse to return a just-enqueued packet, making netem
return NULL despite having backlog, which causes parents like DRR to
incorrectly deactivate the class.
Move all time-ready packets into the child before calling its dequeue.
This separates the enqueue and dequeue phases so the parent sees
consistent qlen transitions.
> diff --git a/net/sched/sch_netem.c b/net/sched/sch_netem.c
> index e264f7aefb97..b93f0e886a2b 100644
> --- a/net/sched/sch_netem.c
> +++ b/net/sched/sch_netem.c
[ ... ]
> @@ -743,31 +745,28 @@ static struct sk_buff *netem_dequeue_child(struct Qdisc *sch)
> u64 now = ktime_get_ns();
> struct sk_buff *skb;
>
> - skb = netem_peek(q);
> - if (skb) {
> - u64 time_to_send = netem_skb_cb(skb)->time_to_send;
> -
> - if (q->slot.slot_next && q->slot.slot_next < time_to_send)
> - get_slot_next(q, now);
> -
> - if (time_to_send <= now && q->slot.slot_next <= now) {
> - struct sk_buff *to_free = NULL;
> - unsigned int pkt_len;
> - int err;
> + while ((skb = netem_peek(q)) != NULL) {
> + struct sk_buff *to_free = NULL;
> + unsigned int pkt_len;
> + int err;
>
> - skb = netem_pull_tfifo(q, sch);
> - netem_slot_account(q, skb, now);
> + if (netem_skb_cb(skb)->time_to_send > now)
> + break;
> + if (q->slot.slot_next && q->slot.slot_next > now)
> + break;
Does the batch transfer preserve slot gap behavior?
The old code called get_slot_next(q, now) when a slot expired by time:
if (q->slot.slot_next && q->slot.slot_next < time_to_send)
get_slot_next(q, now);
This advanced the slot to a new future window (slot_next = now + next_delay),
enforcing inter-slot gaps when a slot expired by time rather than by packet
or byte budget exhaustion.
The new batching loop only checks 'q->slot.slot_next > now' to break, and
relies solely on netem_slot_account() -> get_slot_next() for slot advancement
(which only triggers when packets_left or bytes_left reach zero).
When a slot expires by time but still has remaining budget, the new code
transfers packets immediately without creating a new inter-slot gap.
netem_dequeue_direct() still has the old slot gap logic:
if (q->slot.slot_next && q->slot.slot_next < time_to_send)
get_slot_next(q, now);
Should netem_dequeue_child() call get_slot_next() before the batching loop
to maintain consistent slot-based traffic shaping semantics between the
direct and child paths?
>
> - pkt_len = qdisc_pkt_len(skb);
> - err = qdisc_enqueue(skb, q->qdisc, &to_free);
> - kfree_skb_list(to_free);
> - if (err != NET_XMIT_SUCCESS) {
> - if (net_xmit_drop_count(err))
> - qdisc_qstats_drop(sch);
> - sch->qstats.backlog -= pkt_len;
> - sch->q.qlen--;
> - qdisc_tree_reduce_backlog(sch, 1, pkt_len);
> - }
> + skb = netem_pull_tfifo(q, sch);
> + netem_slot_account(q, skb, now);
> +
> + pkt_len = qdisc_pkt_len(skb);
> + err = qdisc_enqueue(skb, q->qdisc, &to_free);
> + kfree_skb_list(to_free);
> + if (unlikely(err != NET_XMIT_SUCCESS)) {
> + if (net_xmit_drop_count(err))
> + qdisc_qstats_drop(sch);
> + sch->qstats.backlog -= pkt_len;
> + sch->q.qlen--;
> + qdisc_tree_reduce_backlog(sch, 1, pkt_len);
> }
> }
^ permalink raw reply
* [PATCH net v6 1/2] net, bpf: fix null-ptr-deref in xdp_master_redirect() for down master
From: Jiayuan Chen @ 2026-04-10 11:37 UTC (permalink / raw)
To: netdev
Cc: Jiayuan Chen, syzbot+80e046b8da2820b6ba73, Daniel Borkmann,
Alexei Starovoitov, Andrii Nakryiko, Martin KaFai Lau,
Eduard Zingerman, Song Liu, Yonghong Song, John Fastabend,
KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Jesper Dangaard Brouer, Shuah Khan, Jussi Maki, bpf, linux-kernel,
linux-kselftest
In-Reply-To: <20260410113726.368111-1-jiayuan.chen@linux.dev>
syzkaller reported a kernel panic in bond_rr_gen_slave_id() reached via
xdp_master_redirect(). Full decoded trace:
https://syzkaller.appspot.com/bug?extid=80e046b8da2820b6ba73
bond_rr_gen_slave_id() dereferences bond->rr_tx_counter, a per-CPU
counter that bonding only allocates in bond_open() when the mode is
round-robin. If the bond device was never brought up, rr_tx_counter
stays NULL.
The XDP redirect path can still reach that code on a bond that was
never opened: bpf_master_redirect_enabled_key is a global static key,
so as soon as any bond device has native XDP attached, the
XDP_TX -> xdp_master_redirect() interception is enabled for every
slave system-wide. The path xdp_master_redirect() ->
bond_xdp_get_xmit_slave() -> bond_xdp_xmit_roundrobin_slave_get() ->
bond_rr_gen_slave_id() then runs against a bond that has no
rr_tx_counter and crashes.
Fix this in the generic xdp_master_redirect() by refusing to call into
the master's ->ndo_xdp_get_xmit_slave() when the master device is not
up. IFF_UP is only set after ->ndo_open() has successfully returned,
so this reliably excludes masters whose XDP state has not been fully
initialized. Drop the frame with XDP_ABORTED so the exception is
visible via trace_xdp_exception() rather than silently falling through.
This is not specific to bonding: any current or future master that
defers XDP state allocation to ->ndo_open() is protected.
Fixes: 879af96ffd72 ("net, core: Add support for XDP redirection to slave device")
Reported-by: syzbot+80e046b8da2820b6ba73@syzkaller.appspotmail.com
Closes: https://lore.kernel.org/all/698f84c6.a70a0220.2c38d7.00cc.GAE@google.com/T/
Suggested-by: Daniel Borkmann <daniel@iogearbox.net>
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
net/core/filter.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/net/core/filter.c b/net/core/filter.c
index cf2113af4bc9..9ec70c4b7723 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -4398,6 +4398,8 @@ u32 xdp_master_redirect(struct xdp_buff *xdp)
struct net_device *master, *slave;
master = netdev_master_upper_dev_get_rcu(xdp->rxq->dev);
+ if (unlikely(!(master->flags & IFF_UP)))
+ return XDP_ABORTED;
slave = master->netdev_ops->ndo_xdp_get_xmit_slave(master, xdp);
if (slave && slave != xdp->rxq->dev) {
/* The target device is different from the receiving device, so
--
2.43.0
^ permalink raw reply related
* [PATCH net v6 0/2] net,bpf: fix null-ptr-deref in xdp_master_redirect() for bonding and add selftest
From: Jiayuan Chen @ 2026-04-10 11:37 UTC (permalink / raw)
To: netdev
Cc: Jiayuan Chen, Martin KaFai Lau, Daniel Borkmann, John Fastabend,
Stanislav Fomichev, Alexei Starovoitov, Andrii Nakryiko,
Eduard Zingerman, Song Liu, Yonghong Song, KP Singh, Hao Luo,
Jiri Olsa, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jesper Dangaard Brouer, Shuah Khan,
Jussi Maki, bpf, linux-kernel, linux-kselftest
From: Jiayuan Chen <jiayuan.chen@shopee.com>
This series has gone through several rounds of discussion and the
maintainers hold different views on where the fix should live (in the
generic xdp_master_redirect() path vs. inside bonding). I respect all
of the suggestions, but I would like to get the crash fixed first, so
this version takes the approach of checking whether the master device
is up in xdp_master_redirect(), as suggested by Daniel Borkmann. If a
different shape is preferred later it can be done as a follow-up, but
the null-ptr-deref should not linger.
syzkaller reported a kernel panic, full decoded trace here:
https://syzkaller.appspot.com/bug?extid=80e046b8da2820b6ba73
Problem Description
bond_rr_gen_slave_id() dereferences bond->rr_tx_counter without a NULL
check. rr_tx_counter is a per-CPU counter that bonding only allocates
in bond_open() when the mode is round-robin. If the bond device was
never brought up, rr_tx_counter stays NULL.
The XDP redirect path can still reach that code on a bond that was
never opened: bpf_master_redirect_enabled_key is a global static key,
so as soon as any bond device has native XDP attached, the
XDP_TX -> xdp_master_redirect() interception is enabled for every
slave system-wide. The path xdp_master_redirect() ->
bond_xdp_get_xmit_slave() -> bond_xdp_xmit_roundrobin_slave_get() ->
bond_rr_gen_slave_id() then runs against a bond that has no
rr_tx_counter and crashes.
Solution
Patch 1: Fix this in the generic xdp_master_redirect() by skipping
master interception when the master device is not running. Returning
XDP_TX keeps the original XDP_TX behaviour on the receiving slave, and
avoids calling into any master ->ndo_xdp_get_xmit_slave() on a device
that has not fully initialized its XDP state. This is not specific to
bonding: any current or future master that defers XDP state allocation
to ->ndo_open() is protected.
Patch 2: Add a selftest that reproduces the above scenario.
Changes since v5:
https://lore.kernel.org/netdev/20260309030659.xxxxx-1-jiayuan.chen@linux.dev/
- Moved the fix back into xdp_master_redirect() and use netif_running()
on the master device to decide whether to intercept
(Suggested by Daniel Borkmann, seconded by Paolo Abeni and Eric Dumazet)
Changes since v4:
https://lore.kernel.org/netdev/20260304074301.35482-1-jiayuan.chen@linux.dev/
- Reverted unconditional alloc in bond_init(); instead add a NULL check
with unlikely()/READ_ONCE() in bond_rr_gen_slave_id() and WRITE_ONCE()
in bond_open(), avoiding memory waste for non-RR modes
(Suggested by Nikolay Aleksandrov, patch by Jay Vosburgh)
Changes since v3:
https://lore.kernel.org/netdev/20260228021918.141002-1-jiayuan.chen@linux.dev/T/#t
- Added code comment and commit log explaining why rr_tx_counter is
allocated unconditionally for all modes (Suggested by Jay Vosburgh)
Changes since v2:
https://lore.kernel.org/netdev/20260227092254.272603-1-jiayuan.chen@linux.dev/T/#t
- Moved allocation from bond_create_init() helper into bond_init()
(ndo_init), which is the natural single point covering both creation
paths and also handles post-creation mode changes to round-robin
Changes since v1:
https://lore.kernel.org/netdev/20260224112545.37888-1-jiayuan.chen@linux.dev/T/#t
- Moved the guard for NULL rr_tx_counter from xdp_master_redirect()
into the bonding subsystem itself
(Suggested by Sebastian Andrzej Siewior <bigeasy@linutronix.de>)
[1] https://syzkaller.appspot.com/bug?extid=80e046b8da2820b6ba73
Jiayuan Chen (2):
net, bpf: fix null-ptr-deref in xdp_master_redirect() for down master
selftests/bpf: add test for xdp_master_redirect with bond not up
net/core/filter.c | 2 +
.../selftests/bpf/prog_tests/xdp_bonding.c | 101 +++++++++++++++++-
2 files changed, 101 insertions(+), 2 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH net v6 2/2] selftests/bpf: add test for xdp_master_redirect with bond not up
From: Jiayuan Chen @ 2026-04-10 11:37 UTC (permalink / raw)
To: netdev
Cc: Jiayuan Chen, Martin KaFai Lau, Daniel Borkmann, John Fastabend,
Stanislav Fomichev, Alexei Starovoitov, Andrii Nakryiko,
Eduard Zingerman, Song Liu, Yonghong Song, KP Singh, Hao Luo,
Jiri Olsa, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jesper Dangaard Brouer, Shuah Khan,
Jussi Maki, bpf, linux-kernel, linux-kselftest
In-Reply-To: <20260410113726.368111-1-jiayuan.chen@linux.dev>
Add a selftest that reproduces the null-ptr-deref in
bond_rr_gen_slave_id() when XDP redirect targets a bond device in
round-robin mode that was never brought up. The test verifies the fix
by ensuring no crash occurs.
Test setup:
- bond0: active-backup mode, UP, with native XDP (enables
bpf_master_redirect_enabled_key globally)
- bond1: round-robin mode, never UP
- veth1: slave of bond1, with generic XDP (XDP_TX)
- BPF_PROG_TEST_RUN with live frames triggers the redirect path
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
.../selftests/bpf/prog_tests/xdp_bonding.c | 101 +++++++++++++++++-
1 file changed, 99 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c b/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c
index e8ea26464349..0d4ec1e5b401 100644
--- a/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c
+++ b/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c
@@ -191,13 +191,18 @@ static int bonding_setup(struct skeletons *skeletons, int mode, int xmit_policy,
return -1;
}
-static void bonding_cleanup(struct skeletons *skeletons)
+static void link_cleanup(struct skeletons *skeletons)
{
- restore_root_netns();
while (skeletons->nlinks) {
skeletons->nlinks--;
bpf_link__destroy(skeletons->links[skeletons->nlinks]);
}
+}
+
+static void bonding_cleanup(struct skeletons *skeletons)
+{
+ restore_root_netns();
+ link_cleanup(skeletons);
ASSERT_OK(system("ip link delete bond1"), "delete bond1");
ASSERT_OK(system("ip link delete veth1_1"), "delete veth1_1");
ASSERT_OK(system("ip link delete veth1_2"), "delete veth1_2");
@@ -493,6 +498,95 @@ static void test_xdp_bonding_nested(struct skeletons *skeletons)
system("ip link del bond_nest2");
}
+/*
+ * Test that XDP redirect via xdp_master_redirect() does not crash when
+ * the bond master device is not up. When bond is in round-robin mode but
+ * never opened, rr_tx_counter is NULL.
+ */
+static void test_xdp_bonding_redirect_no_up(struct skeletons *skeletons)
+{
+ struct nstoken *nstoken = NULL;
+ int xdp_pass_fd, xdp_tx_fd;
+ int veth1_ifindex;
+ int err;
+ char pkt[ETH_HLEN + 1];
+ struct xdp_md ctx_in = {};
+
+ DECLARE_LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .data_in = &pkt,
+ .data_size_in = sizeof(pkt),
+ .ctx_in = &ctx_in,
+ .ctx_size_in = sizeof(ctx_in),
+ .flags = BPF_F_TEST_XDP_LIVE_FRAMES,
+ .repeat = 1,
+ .batch_size = 1,
+ );
+
+ /* We can't use bonding_setup() because bond will be active */
+ SYS(out, "ip netns add ns_rr_no_up");
+ nstoken = open_netns("ns_rr_no_up");
+ if (!ASSERT_OK_PTR(nstoken, "open ns_rr_no_up"))
+ goto out;
+
+ /* bond0: active-backup, UP with slave veth0.
+ * Attaching native XDP to bond0 enables bpf_master_redirect_enabled_key
+ * globally.
+ */
+ SYS(out, "ip link add bond0 type bond mode active-backup");
+ SYS(out, "ip link add veth0 type veth peer name veth0p");
+ SYS(out, "ip link set veth0 master bond0");
+ SYS(out, "ip link set bond0 up");
+ SYS(out, "ip link set veth0p up");
+
+ /* bond1: round-robin, never UP -> rr_tx_counter stays NULL */
+ SYS(out, "ip link add bond1 type bond mode balance-rr");
+ SYS(out, "ip link add veth1 type veth peer name veth1p");
+ SYS(out, "ip link set veth1 master bond1");
+
+ veth1_ifindex = if_nametoindex("veth1");
+ if (!ASSERT_GT(veth1_ifindex, 0, "veth1_ifindex"))
+ goto out;
+
+ /* Attach native XDP to bond0 -> enables global redirect key */
+ if (xdp_attach(skeletons, skeletons->xdp_tx->progs.xdp_tx, "bond0"))
+ goto out;
+
+ /* Attach generic XDP (XDP_TX) to veth1.
+ * When packets arrive at veth1 via netif_receive_skb, do_xdp_generic()
+ * runs this program. XDP_TX + bond slave triggers xdp_master_redirect().
+ */
+ xdp_tx_fd = bpf_program__fd(skeletons->xdp_tx->progs.xdp_tx);
+ if (!ASSERT_GE(xdp_tx_fd, 0, "xdp_tx prog_fd"))
+ goto out;
+
+ err = bpf_xdp_attach(veth1_ifindex, xdp_tx_fd,
+ XDP_FLAGS_SKB_MODE, NULL);
+ if (!ASSERT_OK(err, "attach generic XDP to veth1"))
+ goto out;
+
+ /* Run BPF_PROG_TEST_RUN with XDP_PASS live frames on veth1.
+ * XDP_PASS frames become SKBs with skb->dev = veth1, entering
+ * netif_receive_skb -> do_xdp_generic -> xdp_master_redirect.
+ * Without the fix, bond_rr_gen_slave_id() dereferences NULL
+ * rr_tx_counter and crashes.
+ */
+ xdp_pass_fd = bpf_program__fd(skeletons->xdp_dummy->progs.xdp_dummy_prog);
+ if (!ASSERT_GE(xdp_pass_fd, 0, "xdp_pass prog_fd"))
+ goto out;
+
+ memset(pkt, 0, sizeof(pkt));
+ ctx_in.data_end = sizeof(pkt);
+ ctx_in.ingress_ifindex = veth1_ifindex;
+
+ err = bpf_prog_test_run_opts(xdp_pass_fd, &opts);
+ ASSERT_OK(err, "xdp_pass test_run should not crash");
+
+out:
+ link_cleanup(skeletons);
+ close_netns(nstoken);
+ SYS_NOFAIL("ip netns del ns_rr_no_up");
+}
+
static void test_xdp_bonding_features(struct skeletons *skeletons)
{
LIBBPF_OPTS(bpf_xdp_query_opts, query_opts);
@@ -738,6 +832,9 @@ void serial_test_xdp_bonding(void)
if (test__start_subtest("xdp_bonding_redirect_multi"))
test_xdp_bonding_redirect_multi(&skeletons);
+ if (test__start_subtest("xdp_bonding_redirect_no_up"))
+ test_xdp_bonding_redirect_no_up(&skeletons);
+
out:
xdp_dummy__destroy(skeletons.xdp_dummy);
xdp_tx__destroy(skeletons.xdp_tx);
--
2.43.0
^ permalink raw reply related
* Re: [PATCH net v4 5/8] net/sched: netem: batch-transfer ready packets to avoid child re-entrancy
From: Simon Horman @ 2026-04-10 11:39 UTC (permalink / raw)
To: Stephen Hemminger
Cc: netdev, Jamal Hadi Salim, Jiri Pirko, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, open list
In-Reply-To: <20260406172627.210894-6-stephen@networkplumber.org>
On Mon, Apr 06, 2026 at 10:25:13AM -0700, Stephen Hemminger wrote:
> netem_dequeue_child() previously transferred one packet from the tfifo
> to the child qdisc per dequeue call. Parents like HFSC that track
> class active/inactive state on qlen transitions could see an enqueue
> during dequeue, causing double-insertion into the eltree
> (CVE-2025-37890, CVE-2025-38001). Non-work-conserving children like
> TBF could also refuse to return a just-enqueued packet, making netem
> return NULL despite having backlog, which causes parents like DRR to
> incorrectly deactivate the class.
>
> Move all time-ready packets into the child before calling its dequeue.
> This separates the enqueue and dequeue phases so the parent sees
> consistent qlen transitions.
>
> Fixes: 50612537e9ab ("netem: fix classful handling")
>
nit: no blank line here
> Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
I forwarded an AI generated review separately.
Because I couldn't convince myself it wasn't valid.
...
^ permalink raw reply
* Re: [PATCH net v4 4/8] net/sched: netem: refactor dequeue into helper functions
From: Simon Horman @ 2026-04-10 11:39 UTC (permalink / raw)
To: Stephen Hemminger
Cc: netdev, Jamal Hadi Salim, Jiri Pirko, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, open list
In-Reply-To: <20260406172627.210894-5-stephen@networkplumber.org>
On Mon, Apr 06, 2026 at 10:25:12AM -0700, Stephen Hemminger wrote:
> Extract the tfifo removal, slot accounting, and child/direct dequeue
> paths from the monolithic netem_dequeue() into separate helpers:
>
> netem_pull_tfifo() - remove head packet from tfifo
> netem_slot_account() - update slot pacing counters
> netem_dequeue_child() - enqueue to child, then dequeue from child
> netem_dequeue_direct()- dequeue from tfifo when no child
>
> This replaces the goto-based control flow with straightforward function
> calls, making the code easier to follow and modify.
>
> No functional change intended.
Manually verified (as best I could :)
>
> Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
Thanks for splitting this out.
It was very helpful.
Reviewed-by: Simon Horman <horms@kernel.org>
^ permalink raw reply
* [PATCH net-next v3 1/9] net: dsa: add tag driver for LAN9645X
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
In-Reply-To: <20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com>
Add tag driver for LAN9645x using a front port as CPU port. This mode
is called an NPI port in the datasheet.
Use long prefix on extraction (RX) and no prefix on injection (TX). A
long prefix on extraction helps get through the conduit port on host
side, since it will see a broadcast MAC.
The LAN9645x chip is in the same design architecture family as ocelot
and lan966x. The tagging protocol has the same structure as these chips,
but the particular fields are different or have different sizes.
Therefore, this tag driver is similar to tag_ocelot.c, but the
differences in fields makes it hard to reuse.
LAN9645x supports 3 different tag formats for extraction/injection of
frames from a CPU port: long prefix, short prefix and no prefix.
The tag is prepended to the frame. The critical data for the chip is
contained in an internal frame header (IFH) which is 28 bytes. The
prefix formats look like this:
Long prefix (16 bytes) + IFH:
- DMAC = 0xffffffffffff on extraction.
- SMAC = 0xfeffffffffff on extraction.
- ETYPE = 0x8880
- payload = 0x0011
- IFH
Short prefix (4 bytes) + IFH:
- 0x8880
- 0x0011
- IFH
No prefix:
- IFH
The format can be configured asymmetrically on RX and TX.
The IFH get/set functions are declared as inline. All the field
constants are compile-time known, so when these calls are inlined
efficient code is generated with branches pruned and loops unrolled.
During testing it was observed that without explicit inlining GCC would
have trouble inlining the functions, which hurt performance.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- guard vlan_remove_tag behind skb_headlen(skb) >= VLAN_ETH_HLEN on xmit
- add pskb_may_pull checks in rx path
Changes in v2:
- sorting in net/dsa/Kconfig
- sorting in net/dsa/Makefile
- remove default zero promisc_on_conduit
- move functions to to .c file
- add justification for inline usage to commit message
- add __skb_put_padto on xmit path
- fix hwaccel_put_tag
---
MAINTAINERS | 8 ++
include/linux/dsa/lan9645x.h | 134 ++++++++++++++++++++
include/net/dsa.h | 2 +
net/dsa/Kconfig | 11 ++
net/dsa/Makefile | 1 +
net/dsa/tag_lan9645x.c | 286 +++++++++++++++++++++++++++++++++++++++++++
6 files changed, 442 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 9d1e6d3acbac..a8e4b5f5da3c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17278,6 +17278,14 @@ L: netdev@vger.kernel.org
S: Maintained
F: drivers/net/phy/microchip_t1.c
+MICROCHIP LAN9645X ETHERNET SWITCH DRIVER
+M: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
+M: UNGLinuxDriver@microchip.com
+L: netdev@vger.kernel.org
+S: Maintained
+F: include/linux/dsa/lan9645x.h
+F: net/dsa/tag_lan9645x.c
+
MICROCHIP LAN966X ETHERNET DRIVER
M: Horatiu Vultur <horatiu.vultur@microchip.com>
M: UNGLinuxDriver@microchip.com
diff --git a/include/linux/dsa/lan9645x.h b/include/linux/dsa/lan9645x.h
new file mode 100644
index 000000000000..34c18bf975d0
--- /dev/null
+++ b/include/linux/dsa/lan9645x.h
@@ -0,0 +1,134 @@
+/* SPDX-License-Identifier: GPL-2.0
+ * Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#ifndef _NET_DSA_TAG_LAN9645X_H_
+#define _NET_DSA_TAG_LAN9645X_H_
+
+#include <net/dsa.h>
+
+/* LAN9645x supports 3 different formats on an NPI port, long prefix, short
+ * prefix and no prefix. The format can be configured asymmetrically on RX and
+ * TX. We use long prefix on extraction (RX), and no prefix on injection.
+ * The long prefix on extraction helps get through the conduit port on host
+ * side, since it will see a broadcast MAC.
+ *
+ * The internal frame header (IFH) is 28 bytes, and the fields are documented
+ * below.
+ *
+ * Long prefix, 16 bytes + IFH:
+ * - DMAC = 0xFFFFFFFFFFFF on extraction.
+ * - SMAC = 0xFEFFFFFFFFFF on extraction.
+ * - ETYPE = 0x8880
+ * - payload = 0x0011
+ * - IFH
+ *
+ * Short prefix, 4 bytes + IFH:
+ * - 0x8880
+ * - 0x0011
+ * - IFH
+ *
+ * No prefix:
+ * - IFH
+ *
+ */
+#define LAN9645X_IFH_TAG_TYPE_C 0
+#define LAN9645X_IFH_TAG_TYPE_S 1
+#define LAN9645X_IFH_LEN_U32 7
+#define LAN9645X_IFH_LEN (LAN9645X_IFH_LEN_U32 * sizeof(u32))
+#define LAN9645X_IFH_BITS (LAN9645X_IFH_LEN * BITS_PER_BYTE)
+#define LAN9645X_SHORT_PREFIX_LEN 4
+#define LAN9645X_LONG_PREFIX_LEN 16
+#define LAN9645X_TOTAL_TAG_LEN (LAN9645X_LONG_PREFIX_LEN + LAN9645X_IFH_LEN)
+
+#define IFH_INJ_TIMESTAMP 192
+#define IFH_BYPASS 191
+#define IFH_MASQ 190
+#define IFH_TIMESTAMP 186
+#define IFH_TIMESTAMP_NS 194
+#define IFH_TIMESTAMP_SUBNS 186
+#define IFH_MASQ_PORT 186
+#define IFH_RCT_INJ 185
+#define IFH_LEN 171
+#define IFH_WRDMODE 169
+#define IFH_RTAGD 167
+#define IFH_CUTTHRU 166
+#define IFH_REW_CMD 156
+#define IFH_REW_OAM 155
+#define IFH_PDU_TYPE 151
+#define IFH_FCS_UPD 150
+#define IFH_DP 149
+#define IFH_RTE_INB_UPDATE 148
+#define IFH_POP_CNT 146
+#define IFH_ETYPE_OFS 144
+#define IFH_SRCPORT 140
+#define IFH_SEQ_NUM 120
+#define IFH_TAG_TYPE 119
+#define IFH_TCI 103
+#define IFH_DSCP 97
+#define IFH_QOS_CLASS 94
+#define IFH_CPUQ 86
+#define IFH_LEARN_FLAGS 84
+#define IFH_SFLOW_ID 80
+#define IFH_ACL_HIT 79
+#define IFH_ACL_IDX 73
+#define IFH_ISDX 65
+#define IFH_DSTS 55
+#define IFH_FLOOD 53
+#define IFH_SEQ_OP 51
+#define IFH_IPV 48
+#define IFH_AFI 47
+#define IFH_RTP_ID 37
+#define IFH_RTP_SUBID 36
+#define IFH_PN_DATA_STATUS 28
+#define IFH_PN_TRANSF_STATUS_ZERO 27
+#define IFH_PN_CC 11
+#define IFH_DUPL_DISC_ENA 10
+#define IFH_RCT_AVAIL 9
+
+#define IFH_INJ_TIMESTAMP_SZ 32
+#define IFH_BYPASS_SZ 1
+#define IFH_MASQ_SZ 1
+#define IFH_TIMESTAMP_SZ 38
+#define IFH_TIMESTAMP_NS_SZ 30
+#define IFH_TIMESTAMP_SUBNS_SZ 8
+#define IFH_MASQ_PORT_SZ 4
+#define IFH_RCT_INJ_SZ 1
+#define IFH_LEN_SZ 14
+#define IFH_WRDMODE_SZ 2
+#define IFH_RTAGD_SZ 2
+#define IFH_CUTTHRU_SZ 1
+#define IFH_REW_CMD_SZ 10
+#define IFH_REW_OAM_SZ 1
+#define IFH_PDU_TYPE_SZ 4
+#define IFH_FCS_UPD_SZ 1
+#define IFH_DP_SZ 1
+#define IFH_RTE_INB_UPDATE_SZ 1
+#define IFH_POP_CNT_SZ 2
+#define IFH_ETYPE_OFS_SZ 2
+#define IFH_SRCPORT_SZ 4
+#define IFH_SEQ_NUM_SZ 16
+#define IFH_TAG_TYPE_SZ 1
+#define IFH_TCI_SZ 16
+#define IFH_DSCP_SZ 6
+#define IFH_QOS_CLASS_SZ 3
+#define IFH_CPUQ_SZ 8
+#define IFH_LEARN_FLAGS_SZ 2
+#define IFH_SFLOW_ID_SZ 4
+#define IFH_ACL_HIT_SZ 1
+#define IFH_ACL_IDX_SZ 6
+#define IFH_ISDX_SZ 8
+#define IFH_DSTS_SZ 10
+#define IFH_FLOOD_SZ 2
+#define IFH_SEQ_OP_SZ 2
+#define IFH_IPV_SZ 3
+#define IFH_AFI_SZ 1
+#define IFH_RTP_ID_SZ 10
+#define IFH_RTP_SUBID_SZ 1
+#define IFH_PN_DATA_STATUS_SZ 8
+#define IFH_PN_TRANSF_STATUS_ZERO_SZ 1
+#define IFH_PN_CC_SZ 16
+#define IFH_DUPL_DISC_ENA_SZ 1
+#define IFH_RCT_AVAIL_SZ 1
+
+#endif /* _NET_DSA_TAG_LAN9645X_H_ */
diff --git a/include/net/dsa.h b/include/net/dsa.h
index 8b6d34e8a6f0..f900dc0e1301 100644
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -58,6 +58,7 @@ struct tc_action;
#define DSA_TAG_PROTO_YT921X_VALUE 30
#define DSA_TAG_PROTO_MXL_GSW1XX_VALUE 31
#define DSA_TAG_PROTO_MXL862_VALUE 32
+#define DSA_TAG_PROTO_LAN9645X_VALUE 33
enum dsa_tag_protocol {
DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
@@ -93,6 +94,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_YT921X = DSA_TAG_PROTO_YT921X_VALUE,
DSA_TAG_PROTO_MXL_GSW1XX = DSA_TAG_PROTO_MXL_GSW1XX_VALUE,
DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
+ DSA_TAG_PROTO_LAN9645X = DSA_TAG_PROTO_LAN9645X_VALUE,
};
struct dsa_switch;
diff --git a/net/dsa/Kconfig b/net/dsa/Kconfig
index 5ed8c704636d..f3facb12e96e 100644
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -75,6 +75,17 @@ config NET_DSA_TAG_HELLCREEK
Say Y or M if you want to enable support for tagging frames
for the Hirschmann Hellcreek TSN switches.
+config NET_DSA_TAG_LAN9645X
+ tristate "Tag driver for Lan9645x switches"
+ help
+ Say Y or M if you want to enable NPI tagging for the Lan9645x switches.
+ In this mode, the frames over the Ethernet CPU port are prepended with
+ a hardware-defined injection/extraction frame header.
+ On injection a 28 byte internal frame header (IFH) is used. On
+ extraction a 16 byte prefix is prepended before the internal frame
+ header. This prefix starts with a broadcast MAC, to ease passage
+ through the host side RX filter.
+
config NET_DSA_TAG_GSWIP
tristate "Tag driver for Lantiq / Intel GSWIP switches"
help
diff --git a/net/dsa/Makefile b/net/dsa/Makefile
index bf7247759a64..ca1e2dc90b80 100644
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -27,6 +27,7 @@ obj-$(CONFIG_NET_DSA_TAG_GSWIP) += tag_gswip.o
obj-$(CONFIG_NET_DSA_TAG_HELLCREEK) += tag_hellcreek.o
obj-$(CONFIG_NET_DSA_TAG_KSZ) += tag_ksz.o
obj-$(CONFIG_NET_DSA_TAG_LAN9303) += tag_lan9303.o
+obj-$(CONFIG_NET_DSA_TAG_LAN9645X) += tag_lan9645x.o
obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk.o
obj-$(CONFIG_NET_DSA_TAG_MXL_862XX) += tag_mxl862xx.o
obj-$(CONFIG_NET_DSA_TAG_MXL_GSW1XX) += tag_mxl-gsw1xx.o
diff --git a/net/dsa/tag_lan9645x.c b/net/dsa/tag_lan9645x.c
new file mode 100644
index 000000000000..6dae3b9ec240
--- /dev/null
+++ b/net/dsa/tag_lan9645x.c
@@ -0,0 +1,286 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/dsa/lan9645x.h>
+
+#include "tag.h"
+
+#define LAN9645X_NAME "lan9645x"
+
+#define BTM_MSK(n) ((u8)GENMASK(n, 0))
+#define TOP_MSK(n) ((u8)GENMASK(7, n))
+
+static inline void set_merge_mask(u8 *on_zero, u8 on_one, u8 mask)
+{
+ *on_zero = *on_zero ^ ((*on_zero ^ on_one) & mask);
+}
+
+/* The internal frame header (IFH) is a big-endian 28 byte unpadded bit array.
+ * Frames can be prepended with an IFH on injection and extraction. There
+ * are two field layouts, one for extraction and one for injection.
+ *
+ * IFH bits go from high to low, for instance
+ * ifh[0] = [223:216]
+ * ifh[27] = [7:0]
+ *
+ * Here is an example of setting a value starting at bit 13 of bit length 17.
+ *
+ * val = 0x1ff
+ * pos = 13
+ * length = 17
+ *
+ *
+ * IFH[] 0 23 24 25 26 27
+ *
+ * end_u8 start_u8
+ * +--------+----------------+--------+--------+--------+--------+--------+
+ * | | | | | | | |
+ * IFH | | .... | | vvvvvvvvvvvvvvvvvvv | |
+ * | | | | | | | | | |
+ * +--------+----------------+--------+--+-----+--------+--+-----+--------+
+ * Bits 223 39 32 31| 24 23 16 15| 8 7 0
+ * | |
+ * | |
+ * | |
+ * v v
+ * end = 29 pos = 13
+ * end_rem = 5 pos_rem = 5
+ * end_u8 = 3 start_u8 = 1
+ * BTM_MSK(5)= 0x3f TOP_MSK(5) = 0xe0
+ *
+ *
+ * In end_u8 and start_u8 we must merge the existing IFH byte with the new
+ * value. In the 'middle' bytes of the value we can overwrite the corresponding
+ * IFH byte.
+ */
+static inline void lan9645x_ifh_set(u8 *ifh, u32 val, size_t pos, size_t length)
+{
+ size_t end = (pos + length) - 1;
+ size_t end_rem = end & 0x7;
+ size_t pos_rem = pos & 0x7;
+ size_t start_u8 = pos >> 3;
+ size_t end_u8 = end >> 3;
+ u8 end_mask, start_mask;
+ size_t vshift;
+ u8 *ptr;
+
+ BUILD_BUG_ON_MSG(length > 32, "IFH field size wider than 32.");
+ BUILD_BUG_ON_MSG(length == 0, "IFH field size of 0.");
+ BUILD_BUG_ON_MSG(pos + length > LAN9645X_IFH_BITS,
+ "IFH field overflows IFH");
+
+ end_mask = BTM_MSK(end_rem);
+ start_mask = TOP_MSK(pos_rem);
+
+ ptr = &ifh[LAN9645X_IFH_LEN - 1 - end_u8];
+
+ if (end_u8 == start_u8)
+ return set_merge_mask(ptr, val << pos_rem,
+ end_mask & start_mask);
+
+ vshift = length - end_rem - 1;
+ set_merge_mask(ptr++, val >> vshift, end_mask);
+
+ for (size_t j = 1; j < end_u8 - start_u8; j++) {
+ vshift -= 8;
+ *ptr++ = val >> vshift;
+ }
+
+ set_merge_mask(ptr, val << pos_rem, start_mask);
+}
+
+static inline u32 lan9645x_ifh_get(const u8 *ifh, size_t pos, size_t length)
+{
+ size_t end = (pos + length) - 1;
+ size_t end_rem = end & 0x7;
+ size_t pos_rem = pos & 0x7;
+ size_t start_u8 = pos >> 3;
+ size_t end_u8 = end >> 3;
+ u8 end_mask, start_mask;
+ const u8 *ptr;
+ u32 val;
+
+ BUILD_BUG_ON_MSG(length > 32, "IFH field size wider than 32.");
+ BUILD_BUG_ON_MSG(length == 0, "IFH field size of 0.");
+ BUILD_BUG_ON_MSG(pos + length > LAN9645X_IFH_BITS,
+ "IFH field overflows IFH");
+
+ end_mask = BTM_MSK(end_rem);
+ start_mask = TOP_MSK(pos_rem);
+
+ ptr = &ifh[LAN9645X_IFH_LEN - 1 - end_u8];
+
+ if (end_u8 == start_u8)
+ return (*ptr & end_mask & start_mask) >> pos_rem;
+
+ val = *ptr++ & end_mask;
+
+ for (size_t j = 1; j < end_u8 - start_u8; j++)
+ val = val << 8 | *ptr++;
+
+ return val << (8 - pos_rem) | (*ptr & start_mask) >> pos_rem;
+}
+
+static void lan9645x_xmit_get_vlan_info(struct sk_buff *skb,
+ struct net_device *br,
+ u32 *vlan_tci, u32 *tag_type)
+{
+ struct vlan_ethhdr *hdr;
+ u16 proto, tci;
+
+ if (!br || !br_vlan_enabled(br)) {
+ *vlan_tci = 0;
+ *tag_type = LAN9645X_IFH_TAG_TYPE_C;
+ return;
+ }
+
+ hdr = (struct vlan_ethhdr *)skb_mac_header(skb);
+ br_vlan_get_proto(br, &proto);
+
+ if (skb_headlen(skb) >= VLAN_ETH_HLEN &&
+ ntohs(hdr->h_vlan_proto) == proto) {
+ vlan_remove_tag(skb, &tci);
+ *vlan_tci = tci;
+ } else {
+ rcu_read_lock();
+ br_vlan_get_pvid_rcu(br, &tci);
+ rcu_read_unlock();
+ *vlan_tci = tci;
+ }
+
+ *tag_type = (proto != ETH_P_8021Q) ? LAN9645X_IFH_TAG_TYPE_S :
+ LAN9645X_IFH_TAG_TYPE_C;
+}
+
+static struct sk_buff *lan9645x_xmit(struct sk_buff *skb,
+ struct net_device *ndev)
+{
+ struct dsa_port *dp = dsa_user_to_port(ndev);
+ struct dsa_switch *ds = dp->ds;
+ u32 cpu_port = ds->num_ports;
+ u32 vlan_tci, tag_type;
+ u32 qos_class;
+ void *ifh;
+
+ lan9645x_xmit_get_vlan_info(skb, dsa_port_bridge_dev_get(dp), &vlan_tci,
+ &tag_type);
+
+ /* We need to make sure frame has the proper size after IFH is stripped
+ * by hw.
+ */
+ if (__skb_put_padto(skb, ETH_ZLEN, false))
+ return NULL;
+
+ qos_class = netdev_get_num_tc(ndev) ?
+ netdev_get_prio_tc_map(ndev, skb->priority) :
+ skb->priority;
+
+ /* Make room for IFH */
+ ifh = skb_push(skb, LAN9645X_IFH_LEN);
+ memset(ifh, 0, LAN9645X_IFH_LEN);
+
+ lan9645x_ifh_set(ifh, 1, IFH_BYPASS, IFH_BYPASS_SZ);
+ lan9645x_ifh_set(ifh, cpu_port, IFH_SRCPORT, IFH_SRCPORT_SZ);
+ lan9645x_ifh_set(ifh, tag_type, IFH_TAG_TYPE, IFH_TAG_TYPE_SZ);
+ lan9645x_ifh_set(ifh, vlan_tci, IFH_TCI, IFH_TCI_SZ);
+ lan9645x_ifh_set(ifh, qos_class, IFH_QOS_CLASS, IFH_QOS_CLASS_SZ);
+ lan9645x_ifh_set(ifh, BIT(dp->index), IFH_DSTS, IFH_DSTS_SZ);
+
+ return skb;
+}
+
+static struct sk_buff *lan9645x_rcv(struct sk_buff *skb,
+ struct net_device *ndev)
+{
+ u32 src_port, qos_class, vlan_tci, tag_type, popcnt, etype_ofs;
+ struct dsa_port *dp;
+ u32 ifh_gap_len = 0;
+ u16 vlan_tpid;
+ u8 *ifh;
+
+ /* DSA master already consumed DMAC,SMAC,ETYPE from long prefix. Go back
+ * to beginning of frame.
+ */
+ skb_push(skb, ETH_HLEN);
+
+ if (unlikely(!pskb_may_pull(skb, LAN9645X_TOTAL_TAG_LEN)))
+ return NULL;
+
+ /* IFH starts after our long prefix */
+ ifh = skb_pull(skb, LAN9645X_LONG_PREFIX_LEN);
+
+ popcnt = lan9645x_ifh_get(ifh, IFH_POP_CNT, IFH_POP_CNT_SZ);
+ etype_ofs = lan9645x_ifh_get(ifh, IFH_ETYPE_OFS, IFH_ETYPE_OFS_SZ);
+ src_port = lan9645x_ifh_get(ifh, IFH_SRCPORT, IFH_SRCPORT_SZ);
+ tag_type = lan9645x_ifh_get(ifh, IFH_TAG_TYPE, IFH_TAG_TYPE_SZ);
+ vlan_tci = lan9645x_ifh_get(ifh, IFH_TCI, IFH_TCI_SZ);
+ qos_class = lan9645x_ifh_get(ifh, IFH_QOS_CLASS, IFH_QOS_CLASS_SZ);
+
+ /* Set skb->data at start of real header
+ *
+ * Since REW_PORT_NO_REWRITE=0 is required on the NPI port, we need to
+ * account for any tags popped by the hardware, as that will leave a gap
+ * between the IFH and DMAC.
+ */
+ if (popcnt == 0 && etype_ofs == 0)
+ ifh_gap_len = 2 * VLAN_HLEN;
+ else if (popcnt == 3)
+ ifh_gap_len = VLAN_HLEN;
+
+ skb_pull(skb, LAN9645X_IFH_LEN);
+
+ if (unlikely(!pskb_may_pull(skb, ifh_gap_len + ETH_HLEN)))
+ return NULL;
+
+ skb_pull(skb, ifh_gap_len);
+ skb_reset_mac_header(skb);
+ skb_set_network_header(skb, ETH_HLEN);
+ skb_reset_mac_len(skb);
+
+ /* Reset skb->data past the actual ethernet header. */
+ skb_pull(skb, ETH_HLEN);
+ skb_postpull_rcsum(skb,
+ skb->data - LAN9645X_TOTAL_TAG_LEN - ifh_gap_len,
+ LAN9645X_TOTAL_TAG_LEN + ifh_gap_len);
+
+ skb->dev = dsa_conduit_find_user(ndev, 0, src_port);
+ if (WARN_ON_ONCE(!skb->dev)) {
+ /* This should never happen since we have disabled reflection
+ * back to CPU_PORT.
+ */
+ return NULL;
+ }
+
+ dsa_default_offload_fwd_mark(skb);
+
+ skb->priority = qos_class;
+
+ /* While we have REW_PORT_NO_REWRITE=0 on the NPI port, we still disable
+ * port VLAN tagging with REW_TAG_CFG. Any classified VID, different
+ * from a VID in the frame, will not be written to the frame, but is
+ * only communicated via the IFH. So for VLAN-aware ports we add the IFH
+ * vlan to the skb.
+ */
+ dp = dsa_user_to_port(skb->dev);
+ vlan_tpid = tag_type ? ETH_P_8021AD : ETH_P_8021Q;
+
+ if (dsa_port_is_vlan_filtering(dp) && vlan_tci)
+ __vlan_hwaccel_put_tag(skb, htons(vlan_tpid), vlan_tci);
+
+ return skb;
+}
+
+static const struct dsa_device_ops lan9645x_netdev_ops = {
+ .name = LAN9645X_NAME,
+ .proto = DSA_TAG_PROTO_LAN9645X,
+ .xmit = lan9645x_xmit,
+ .rcv = lan9645x_rcv,
+ .needed_headroom = LAN9645X_TOTAL_TAG_LEN,
+};
+
+MODULE_DESCRIPTION("DSA tag driver for LAN9645x family of switches, using NPI port");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_LAN9645X, LAN9645X_NAME);
+
+module_dsa_tag_driver(lan9645x_netdev_ops);
--
2.52.0
^ permalink raw reply related
* [PATCH net-next v3 0/9] net: dsa: add DSA support for the LAN9645x switch chip family
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
This series provides the Microchip LAN9645X Switch driver.
The LAN9645x is a family of chips with ethernet switch functionality and
multiple peripheral functions. The switch delivers up to 9 ethernet
ports and 12 Gbps switching bandwidth.
The switch chip has 5 integrated copper PHYs, support for 2x RGMII
interfaces, 2x SGMII and one QSGMII interface.
The switch chip is from the same design architecture family as ocelot
and lan966x, and the driver reflects this similarity. However, LAN9645x
does not have an internal CPU in any package, and must be driven
externally. For register IO it supports interfaces such as SPI, I2C and
MDIO.
The chip supports a variety of network features such as
* Mactable for MDB/FDB functionality
* Bridge forwarding offload
* VLAN-aware bridging
* IGMP/MLD snooping
* Link aggregation
* PTP timestamping
* FRER (802.1CB)
* Media Redundancy Protocol
* Parallel Redundancy and High-Availability Seamless Redundancy
(HSR/PRP) in DANH/DANP mode
* Per stream filtering and policing
* Shapers such as Credit Based Shaping and Time Aware Shaing
* Frame preemption
* A TCAM (VCAP) for line-rate frame processing
The LAN9645x family consists of the following SKUs:
LAN96455F
LAN96457F
LAN96459F
LAN96455S
LAN96457S
LAN96459S
The difference between the SKUs is the number of supported ports (5, 7
or 9) and features supported. The F subfamily supports HSR/PRP and TSN,
while the S subfamily does not.
The intended way to bind this driver is using a parent MFD driver,
responsible for the register IO protocol, and distributing regmaps to
child devices. The goal is to use the same approach as the MFD driver in
drivers/mfd/ocelot-spi.c.
This driver expects to request named regmaps from a parent device. This
approach is similar to the DSA driver
drivers/net/dsa/ocelot/ocelot_ext.c
which supports being driven by an external CPU via SPI with parent
device drivers/mfd/ocelot-spi.c.
The MFD driver will come in a later series, because there are
requirements on the number of child devices before a driver qualifies as
a MFD device.
Development is done using the LAN966x as a host CPU, running the lan966x
swichdev driver, using the EVB-LAN9668 EDS2 board.
The datasheet is available here:
https://ww1.microchip.com/downloads/aemDocuments/documents/UNG/ProductDocuments/DataSheets/LAN9645xF-Data-Sheet-DS00006065.pdf
This series will deliver the following features:
* Standalone ports
* Bridge forwarding and FDB offloading
* VLAN-aware bridge
* Stats integration
More support will be added at a later stage. Here is a tentative plan of
future patches for this DSA driver:
* Add LAG support.
* Add MDB support.
* Add TC matchall mirror support.
* Add TC matchall police support.
* Add DCB/qos support.
* Add simple TC support: mqprio, cbs, tbf, ebf.
* Add TC flower filter support.
* Add HSR/PRP offloading support.
* Add PTP support.
* Add TC taprio support.
For completeness I include tentative plan of planned patches for
LAN9645x peripherals:
* Extend pinctrl-ocelot for LAN9645x:
https://lore.kernel.org/linux-gpio/20260119-pinctrl_ocelot_extend_support_for_lan9645x-v1-0-1228155ed0ee@microchip.com/
* Add driver for internal PHY:
https://lore.kernel.org/netdev/20260123-phy_micrel_add_support_for_lan9645x_internal_phy-v1-1-8484b1a5a7fd@microchip.com/
* MFD driver for managing register IO protocol and child device
initialization.
* Extend pinctrl-microchip-sgpio for LAN9645x support.
* Extend i2c_designware for LAN9645x support.
* Add driver for outbound interrupt controller.
* Add serdes driver for lan9645x.
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- Individual patches mention specific v3 changes.
- Add guard before vlan_remove_tag on xmit
- Add pskb_may_pull checks on rx
- Remove additionalProperties: true in bindings
- Remove unnecessary | from description in bindings
- Change top level $ref to dsa.yaml#/$defs/ethernet-ports
- Use ethernet-ports and ethernet-port
- Move ethernet-ports under properties instead of patternProperties
- Move unevaluatedProperties: false after $ref
- Update bindings example to use ethernet-ports and ethernet-port
- Move DEV_MAC_TAGS_CFG to port setup, instead of vlan config, so vlan
overhead is always included in port frame maxlen calculation.
- Remove code disabling ipv6 on conduit
- Use of_property_read_u32 for {rx,tx}-internal-delay-ps
- Use dsa_user_ports(ds) instead of
GENMASK(lan9645x->num_phys_ports - 1, 0) as base flood mask.
- Add comment explaining obey vlan
- Allow disabling aging with explicit zero parameters.
- Fix non-forwarding STP states in bridge fwd calculation.
- Restore host flood state on bridge leave.
- Avoid mac_entry dealloc when mac table writes fail.
- Avoid mdb_entry dealloc when mac table writes fail.
- Dealloc mac_entries on deinit.
- Dealloc mdb_entries on deinit.
- Link to v2: https://lore.kernel.org/r/20260324-dsa_lan9645x_switch_driver_base-v2-0-f7504e3b0681@microchip.com
Changes in v2:
- Individual patches have specific v2 changes.
- Ran DSA, and std counters, selftests, which prompted several changes.
The following selftests pass, except for some expected failures:
- bridge_vlan_aware.sh
- bridge_vlan_unaware.sh
- bridge_vlan_mcast.sh
- no_forwarding.sh
- bridge_mdb.sh
- bridge_mld.sh
- test_fdb_stress_test.sh
- .../drivers/net/hw/ethtool_rmon.sh
- .../drivers/net/hw/ethtool_std_stats.sh (from Ioana's series)
- Added new patch for MDB management, as this was required for selftests.
- Added port_set_host_flood to enable unknown traffic to standalone during
promisc/ALL_MULTI (selftests).
- Remove the dubugfs.
- Link to v1: https://lore.kernel.org/r/20260303-dsa_lan9645x_switch_driver_base-v1-0-bff8ca1396f5@microchip.com
---
Jens Emil Schulz Østergaard (9):
net: dsa: add tag driver for LAN9645X
dt-bindings: net: lan9645x: add LAN9645X switch bindings
net: dsa: lan9645x: add autogenerated register macros
net: dsa: lan9645x: add basic dsa driver for LAN9645X
net: dsa: lan9645x: add bridge support
net: dsa: lan9645x: add vlan support
net: dsa: lan9645x: add mac table integration
net: dsa: lan9645x: add mdb management
net: dsa: lan9645x: add port statistics
.../net/dsa/microchip,lan96455s-switch.yaml | 111 ++
MAINTAINERS | 10 +
drivers/net/dsa/Kconfig | 2 +
drivers/net/dsa/microchip/Makefile | 1 +
drivers/net/dsa/microchip/lan9645x/Kconfig | 11 +
drivers/net/dsa/microchip/lan9645x/Makefile | 12 +
drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c | 430 +++++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 993 ++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 450 +++++
drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c | 383 ++++
drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c | 76 +
.../net/dsa/microchip/lan9645x/lan9645x_phylink.c | 381 ++++
drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 204 +++
drivers/net/dsa/microchip/lan9645x/lan9645x_regs.h | 1915 ++++++++++++++++++++
.../net/dsa/microchip/lan9645x/lan9645x_stats.c | 922 ++++++++++
.../net/dsa/microchip/lan9645x/lan9645x_stats.h | 277 +++
drivers/net/dsa/microchip/lan9645x/lan9645x_vlan.c | 378 ++++
include/linux/dsa/lan9645x.h | 145 ++
include/net/dsa.h | 2 +
net/dsa/Kconfig | 11 +
net/dsa/Makefile | 1 +
net/dsa/tag_lan9645x.c | 301 +++
22 files changed, 7016 insertions(+)
---
base-commit: b3e69fc3196fc421e26196e7792f17b0463edc6f
change-id: 20260210-dsa_lan9645x_switch_driver_base-312bbfc37edb
Best regards,
--
Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
^ permalink raw reply
* [PATCH net-next v3 2/9] dt-bindings: net: lan9645x: add LAN9645X switch bindings
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
In-Reply-To: <20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com>
Add bindings for LAN9645X switch. We use a fallback compatible for the
smallest SKU microchip,lan96455s-switch.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- remove additionalProperties: true
- remove unnecessary | from description
- change top level $ref to dsa.yaml#/$defs/ethernet-ports
- use ethernet-ports and ethernet-port
- move ethernet-ports under properties instead of patternProperties
- move unevaluatedProperties: false after $ref
- update example to use ethernet-ports and ethernet-port
Changes in v2:
- rename file to microchip,lan96455s-switch.yaml
- remove led vendor property
- add {rx,tx}-internal-delay-ps for rgmii delay
- remove labels from example
- remove container node from example
---
.../net/dsa/microchip,lan96455s-switch.yaml | 111 +++++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 112 insertions(+)
diff --git a/Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml b/Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
new file mode 100644
index 000000000000..043fb48922b4
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/dsa/microchip,lan96455s-switch.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Microchip LAN9645x Ethernet switch
+
+maintainers:
+ - Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
+
+description:
+ The LAN9645x switch is a multi-port Gigabit AVB/TSN Ethernet switch with
+ five integrated 10/100/1000Base-T PHYs. In addition to the integrated PHYs,
+ it supports up to 2 RGMII/RMII, up to 2 BASE-X/SERDES/2.5GBASE-X and one
+ Quad-SGMII interfaces.
+
+properties:
+ compatible:
+ oneOf:
+ - enum:
+ - microchip,lan96455s-switch
+ - items:
+ - enum:
+ - microchip,lan96455f-switch
+ - microchip,lan96457f-switch
+ - microchip,lan96459f-switch
+ - microchip,lan96457s-switch
+ - microchip,lan96459s-switch
+ - const: microchip,lan96455s-switch
+
+ reg:
+ maxItems: 1
+
+ ethernet-ports:
+ type: object
+ patternProperties:
+ "^ethernet-port@[0-8]$":
+ type: object
+ description: Ethernet switch ports
+
+ $ref: dsa-port.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ rx-internal-delay-ps:
+ const: 2000
+
+ tx-internal-delay-ps:
+ const: 2000
+
+$ref: dsa.yaml#/$defs/ethernet-ports
+
+required:
+ - compatible
+ - reg
+ - ethernet-ports
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ ethernet-switch@4000 {
+ compatible = "microchip,lan96459f-switch", "microchip,lan96455s-switch";
+ reg = <0x4000 0x244>;
+
+ ethernet-ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-port@0 {
+ reg = <0>;
+ phy-mode = "gmii";
+ phy-handle = <&cuphy0>;
+ };
+
+ ethernet-port@1 {
+ reg = <1>;
+ phy-mode = "gmii";
+ phy-handle = <&cuphy1>;
+ };
+
+ ethernet-port@2 {
+ reg = <2>;
+ phy-mode = "gmii";
+ phy-handle = <&cuphy2>;
+ };
+
+ ethernet-port@3 {
+ reg = <3>;
+ phy-mode = "gmii";
+ phy-handle = <&cuphy3>;
+ };
+
+ ethernet-port@7 {
+ reg = <7>;
+ phy-mode = "rgmii";
+ ethernet = <&cpu_host_port>;
+ rx-internal-delay-ps = <2000>;
+ tx-internal-delay-ps = <2000>;
+
+ fixed-link {
+ speed = <1000>;
+ full-duplex;
+ pause;
+ };
+ };
+ };
+ };
+...
+
diff --git a/MAINTAINERS b/MAINTAINERS
index a8e4b5f5da3c..eeb67eed905c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17283,6 +17283,7 @@ M: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
M: UNGLinuxDriver@microchip.com
L: netdev@vger.kernel.org
S: Maintained
+F: Documentation/devicetree/bindings/net/dsa/microchip,lan96455s-switch.yaml
F: include/linux/dsa/lan9645x.h
F: net/dsa/tag_lan9645x.c
--
2.52.0
^ permalink raw reply related
* [PATCH net-next v3 5/9] net: dsa: lan9645x: add bridge support
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
In-Reply-To: <20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com>
Add support for hardware offloading of the bridge. We support a single
bridge device.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- allow disabling aging with explicit zero parameters.
- fix non-forwarding stp states
- fix restore host_flood requests on bridge leave
Changes in v2:
- variable name consistency
- port_set_learning use stp_state before writing to hw
- add set_host_flood for selftests, which need promic/all_multi on
standalone interfaces
---
drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 296 +++++++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 18 ++
2 files changed, 314 insertions(+)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index 3e68542ba81d..e709396c2298 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -28,6 +28,14 @@ static const char *lan9645x_resource_names[NUM_TARGETS + 1] = {
[NUM_TARGETS] = NULL,
};
+struct lan9645x_host_flood_work {
+ struct work_struct work;
+ struct lan9645x *lan9645x;
+ int port;
+ bool uc;
+ bool mc;
+};
+
static int lan9645x_tag_npi_setup(struct dsa_switch *ds)
{
struct dsa_port *dp, *first_cpu_dp = NULL;
@@ -61,7 +69,9 @@ static void lan9645x_teardown(struct dsa_switch *ds)
{
struct lan9645x *lan9645x = ds->priv;
+ destroy_workqueue(lan9645x->owq);
lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
+ mutex_destroy(&lan9645x->fwd_domain_lock);
}
static int lan9645x_change_mtu(struct dsa_switch *ds, int port, int new_mtu)
@@ -145,6 +155,8 @@ static int lan9645x_setup(struct dsa_switch *ds)
return err;
}
+ mutex_init(&lan9645x->fwd_domain_lock);
+
/* Link Aggregation Mode: NETDEV_LAG_HASH_L2 */
lan_wr(ANA_AGGR_CFG_AC_SMAC_ENA |
ANA_AGGR_CFG_AC_DMAC_ENA,
@@ -242,6 +254,11 @@ static int lan9645x_setup(struct dsa_switch *ds)
lan9645x_port_set_tail_drop_wm(lan9645x);
+ lan9645x->owq = alloc_ordered_workqueue("%s-owq", 0,
+ dev_name(lan9645x->dev));
+ if (!lan9645x->owq)
+ return -ENOMEM;
+
ds->mtu_enforcement_ingress = true;
ds->assisted_learning_on_cpu_port = true;
ds->fdb_isolation = true;
@@ -260,6 +277,276 @@ static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
lan9645x_phylink_get_caps(ds->priv, port, config);
}
+static int lan9645x_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+{
+ u32 age_secs = max(1, msecs / MSEC_PER_SEC / 2);
+ struct lan9645x *lan9645x = ds->priv;
+
+ /* Entry is must suffer two aging scans before it is removed, so it is
+ * aged after 2*AGE_PERIOD, and the unit is in seconds.
+ * An age period of 0 disables automatic aging.
+ */
+ lan_rmw(ANA_AUTOAGE_AGE_PERIOD_SET(msecs ? age_secs : 0),
+ ANA_AUTOAGE_AGE_PERIOD,
+ lan9645x, ANA_AUTOAGE);
+ return 0;
+}
+
+static int lan9645x_port_pre_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags flags,
+ struct netlink_ext_ack *extack)
+{
+ if (flags.mask &
+ ~(BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD))
+ return -EINVAL;
+
+ return 0;
+}
+
+static void lan9645x_port_pgid_set(struct lan9645x *lan9645x, u16 pgid,
+ int chip_port, bool enabled)
+{
+ u32 reg_msk, port_msk;
+
+ WARN_ON(chip_port > CPU_PORT);
+
+ port_msk = ANA_PGID_PGID_SET(enabled ? BIT(chip_port) : 0);
+ reg_msk = ANA_PGID_PGID_SET(BIT(chip_port));
+
+ lan_rmw(port_msk, reg_msk, lan9645x, ANA_PGID(pgid));
+}
+
+static void lan9645x_port_set_learning(struct lan9645x *lan9645x, int port,
+ bool enabled)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+
+ p->learn_ena = enabled;
+
+ enabled = enabled && (p->stp_state == BR_STATE_LEARNING ||
+ p->stp_state == BR_STATE_FORWARDING);
+
+ lan_rmw(ANA_PORT_CFG_LEARN_ENA_SET(enabled), ANA_PORT_CFG_LEARN_ENA,
+ lan9645x, ANA_PORT_CFG(port));
+}
+
+static int lan9645x_port_bridge_flags(struct dsa_switch *ds, int port,
+ struct switchdev_brport_flags f,
+ struct netlink_ext_ack *extack)
+{
+ struct lan9645x *lan9645x = ds->priv;
+
+ if (WARN_ON(port == lan9645x->npi))
+ return -EINVAL;
+
+ if (f.mask & BR_LEARNING)
+ lan9645x_port_set_learning(lan9645x, port,
+ !!(f.val & BR_LEARNING));
+
+ if (f.mask & BR_FLOOD)
+ lan9645x_port_pgid_set(lan9645x, PGID_UC, port,
+ !!(f.val & BR_FLOOD));
+
+ if (f.mask & BR_MCAST_FLOOD) {
+ bool ena = !!(f.val & BR_MCAST_FLOOD);
+
+ lan9645x_port_pgid_set(lan9645x, PGID_MC, port, ena);
+ lan9645x_port_pgid_set(lan9645x, PGID_MCIPV4, port, ena);
+ lan9645x_port_pgid_set(lan9645x, PGID_MCIPV6, port, ena);
+ }
+
+ if (f.mask & BR_BCAST_FLOOD)
+ lan9645x_port_pgid_set(lan9645x, PGID_BC, port,
+ !!(f.val & BR_BCAST_FLOOD));
+
+ return 0;
+}
+
+static void lan9645x_update_fwd_mask(struct lan9645x *lan9645x)
+{
+ struct lan9645x_port *p;
+ struct dsa_port *dp;
+
+ lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+ /* Updates the source port PGIDs, making sure frames from p
+ * are only forwarded to ports q != p, where q is relevant to forward
+ */
+ dsa_switch_for_each_available_port(dp, lan9645x->ds) {
+ u32 mask = 0;
+
+ p = lan9645x_to_port(lan9645x, dp->index);
+
+ if (lan9645x_port_is_bridged(p) &&
+ (lan9645x->bridge_fwd_mask & BIT(dp->index))) {
+ mask = lan9645x->bridge_mask &
+ lan9645x->bridge_fwd_mask & ~BIT(dp->index);
+ }
+
+ lan_wr(mask, lan9645x, ANA_PGID(PGID_SRC + dp->index));
+ }
+}
+
+static void __lan9645x_port_mark_host_flood(struct lan9645x *lan9645x, int port,
+ bool uc, bool mc)
+{
+ lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+ if (uc)
+ lan9645x->host_flood_uc_mask |= BIT(port);
+ else
+ lan9645x->host_flood_uc_mask &= ~BIT(port);
+
+ if (mc)
+ lan9645x->host_flood_mc_mask |= BIT(port);
+ else
+ lan9645x->host_flood_mc_mask &= ~BIT(port);
+}
+
+static void __lan9645x_port_set_host_flood(struct lan9645x *lan9645x, int port)
+{
+ bool mc_ena, uc_ena;
+ u16 unbridged;
+
+ lockdep_assert_held(&lan9645x->fwd_domain_lock);
+
+ /* We want promiscuous and all_multi to affect standalone ports, for
+ * debug and test purposes.
+ *
+ * However, the linux bridge is incredibly eager to put bridged ports in
+ * promiscuous mode.
+
+ * This is unfortunate since lan9645x flood masks are global and not per
+ * ingress port. When some port triggers unknown uc/mc to the CPU, the
+ * traffic from any port is forwarded to the CPU.
+ *
+ * If the host CPU is weak, this can cause tremendous stress. Therefore,
+ * we compromise by ignoring this host flood request for bridged ports.
+ */
+ unbridged = ~lan9645x->bridge_mask & GENMASK(NUM_PHYS_PORTS - 1, 0);
+
+ uc_ena = !!(lan9645x->host_flood_uc_mask & unbridged);
+ lan9645x_port_pgid_set(lan9645x, PGID_UC, CPU_PORT, uc_ena);
+
+ mc_ena = !!(lan9645x->host_flood_mc_mask & unbridged);
+ lan9645x_port_pgid_set(lan9645x, PGID_MC, CPU_PORT, mc_ena);
+ lan9645x_port_pgid_set(lan9645x, PGID_MCIPV4, CPU_PORT, mc_ena);
+ lan9645x_port_pgid_set(lan9645x, PGID_MCIPV6, CPU_PORT, mc_ena);
+}
+
+static void lan9645x_host_flood_work_fn(struct work_struct *work)
+{
+ struct lan9645x_host_flood_work *w =
+ container_of(work, struct lan9645x_host_flood_work, work);
+
+ mutex_lock(&w->lan9645x->fwd_domain_lock);
+ __lan9645x_port_mark_host_flood(w->lan9645x, w->port, w->uc, w->mc);
+ __lan9645x_port_set_host_flood(w->lan9645x, w->port);
+ mutex_unlock(&w->lan9645x->fwd_domain_lock);
+ kfree(w);
+}
+
+/* Called in atomic context */
+static void lan9645x_port_set_host_flood(struct dsa_switch *ds, int port,
+ bool uc, bool mc)
+{
+ struct lan9645x *lan9645x = ds->priv;
+ struct lan9645x_host_flood_work *w;
+
+ w = kzalloc_obj(*w, GFP_ATOMIC);
+ if (!w)
+ return;
+
+ INIT_WORK(&w->work, lan9645x_host_flood_work_fn);
+ w->lan9645x = lan9645x;
+ w->port = port;
+ w->uc = uc;
+ w->mc = mc;
+ queue_work(lan9645x->owq, &w->work);
+}
+
+static int lan9645x_port_bridge_join(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge,
+ bool *tx_fwd_offload,
+ struct netlink_ext_ack *extack)
+{
+ struct lan9645x *lan9645x = ds->priv;
+ struct lan9645x_port *p;
+
+ p = lan9645x_to_port(lan9645x, port);
+
+ if (lan9645x->bridge && lan9645x->bridge != bridge.dev) {
+ NL_SET_ERR_MSG_MOD(extack, "Only one bridge supported");
+ return -EBUSY;
+ }
+
+ mutex_lock(&lan9645x->fwd_domain_lock);
+ /* First bridged port sets bridge dev */
+ if (!lan9645x->bridge_mask)
+ lan9645x->bridge = bridge.dev;
+
+ lan9645x->bridge_mask |= BIT(p->chip_port);
+ __lan9645x_port_set_host_flood(lan9645x, port);
+
+ mutex_unlock(&lan9645x->fwd_domain_lock);
+
+ /* Later: stp_state_set updates forwarding */
+
+ return 0;
+}
+
+static void lan9645x_port_bridge_stp_state_set(struct dsa_switch *ds, int port,
+ u8 state)
+{
+ struct lan9645x *lan9645x;
+ struct lan9645x_port *p;
+ bool learn_ena;
+
+ lan9645x = ds->priv;
+ p = lan9645x_to_port(lan9645x, port);
+
+ mutex_lock(&lan9645x->fwd_domain_lock);
+
+ p->stp_state = state;
+
+ if (state == BR_STATE_FORWARDING)
+ lan9645x->bridge_fwd_mask |= BIT(p->chip_port);
+ else
+ lan9645x->bridge_fwd_mask &= ~BIT(p->chip_port);
+
+ learn_ena = (state == BR_STATE_LEARNING ||
+ state == BR_STATE_FORWARDING) && p->learn_ena;
+
+ lan_rmw(ANA_PORT_CFG_LEARN_ENA_SET(learn_ena),
+ ANA_PORT_CFG_LEARN_ENA, lan9645x,
+ ANA_PORT_CFG(p->chip_port));
+
+ lan9645x_update_fwd_mask(lan9645x);
+ mutex_unlock(&lan9645x->fwd_domain_lock);
+}
+
+static void lan9645x_port_bridge_leave(struct dsa_switch *ds, int port,
+ struct dsa_bridge bridge)
+{
+ struct lan9645x *lan9645x = ds->priv;
+ struct lan9645x_port *p;
+
+ p = lan9645x_to_port(lan9645x, port);
+
+ mutex_lock(&lan9645x->fwd_domain_lock);
+
+ lan9645x->bridge_mask &= ~BIT(p->chip_port);
+
+ /* Last port leaving clears bridge dev */
+ if (!lan9645x->bridge_mask)
+ lan9645x->bridge = NULL;
+
+ __lan9645x_port_set_host_flood(lan9645x, port);
+ lan9645x_update_fwd_mask(lan9645x);
+
+ mutex_unlock(&lan9645x->fwd_domain_lock);
+}
+
static const struct dsa_switch_ops lan9645x_switch_ops = {
.get_tag_protocol = lan9645x_get_tag_protocol,
@@ -273,6 +560,15 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
/* MTU */
.port_change_mtu = lan9645x_change_mtu,
.port_max_mtu = lan9645x_get_max_mtu,
+
+ /* Bridge integration */
+ .set_ageing_time = lan9645x_set_ageing_time,
+ .port_pre_bridge_flags = lan9645x_port_pre_bridge_flags,
+ .port_bridge_flags = lan9645x_port_bridge_flags,
+ .port_bridge_join = lan9645x_port_bridge_join,
+ .port_bridge_leave = lan9645x_port_bridge_leave,
+ .port_stp_state_set = lan9645x_port_bridge_stp_state_set,
+ .port_set_host_flood = lan9645x_port_set_host_flood,
};
static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index d8bdcb8a92ed..22576bb8dd52 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -155,6 +155,11 @@ struct lan9645x {
struct dsa_switch *ds;
struct regmap *rmap[NUM_TARGETS];
+ u16 host_flood_uc_mask;
+ u16 host_flood_mc_mask;
+
+ struct workqueue_struct *owq;
+
int shared_queue_sz;
/* NPI chip_port */
@@ -163,6 +168,12 @@ struct lan9645x {
u8 num_phys_ports;
struct lan9645x_port **ports;
+ /* Forwarding Database */
+ struct net_device *bridge; /* Only support single bridge */
+ u16 bridge_mask; /* Mask for bridged ports */
+ u16 bridge_fwd_mask; /* Mask for forwarding bridged ports */
+ struct mutex fwd_domain_lock; /* lock forwarding configuration */
+
int num_port_dis;
bool dd_dis;
bool tsn_dis;
@@ -172,6 +183,8 @@ struct lan9645x_port {
struct lan9645x *lan9645x;
u8 chip_port;
+ u8 stp_state;
+ bool learn_ena;
bool rx_internal_delay;
bool tx_internal_delay;
@@ -224,6 +237,11 @@ static inline struct lan9645x_port *lan9645x_to_port(struct lan9645x *lan9645x,
return lan9645x->ports[port];
}
+static inline bool lan9645x_port_is_bridged(struct lan9645x_port *p)
+{
+ return p && (p->lan9645x->bridge_mask & BIT(p->chip_port));
+}
+
static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
enum lan9645x_target t, int tinst)
{
--
2.52.0
^ permalink raw reply related
* [PATCH net-next v3 4/9] net: dsa: lan9645x: add basic dsa driver for LAN9645X
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
In-Reply-To: <20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com>
Add the LAN9645X basic DSA driver with initialization, parent regmap
requests, port module initialization for NPI, CPU ports and front ports,
and phylink integration for MAC side configuration.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- move DEV_MAC_TAGS_CFG config to port setup, for vlan overhead in port
frame maxlen
- remove code disabling ipv6 on conduit
- use of_property_read_u32 for {rx,tx}-internal-delay-ps
- use dsa_user_ports(ds) instead of
GENMASK(lan9645x->num_phys_ports - 1, 0) as base flood mask.
- update obey vlan comment
Changes in v2:
- source Kconfig from drivers/net/dsa/Kconfig
- sorting in Kconfig and Makefiles
- remove unused struct fields
- remote path delays
- use port_setup and dp->dn instead of DTS parsing
- phylink: split rgmii setup into dll and speed config
- phylink: remove pcs/sgmii/qsgmii related code
- phylink: simplify mac_prepare
- phylink: remove phylink_ops wrappers
- phylink: remove unrelated config from link up
- phylink: reorder functions according to phylink call order
---
drivers/net/dsa/Kconfig | 2 +
drivers/net/dsa/microchip/Makefile | 1 +
drivers/net/dsa/microchip/lan9645x/Kconfig | 11 +
drivers/net/dsa/microchip/lan9645x/Makefile | 8 +
drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 406 +++++++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 334 +++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c | 76 ++++
.../net/dsa/microchip/lan9645x/lan9645x_phylink.c | 381 +++++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 193 ++++++++++
9 files changed, 1412 insertions(+)
diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
index 39fb8ead16b5..bc50557617be 100644
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -72,6 +72,8 @@ config NET_DSA_MV88E6060
source "drivers/net/dsa/microchip/Kconfig"
+source "drivers/net/dsa/microchip/lan9645x/Kconfig"
+
source "drivers/net/dsa/mv88e6xxx/Kconfig"
source "drivers/net/dsa/mxl862xx/Kconfig"
diff --git a/drivers/net/dsa/microchip/Makefile b/drivers/net/dsa/microchip/Makefile
index 9347cfb3d0b5..e75f17888f75 100644
--- a/drivers/net/dsa/microchip/Makefile
+++ b/drivers/net/dsa/microchip/Makefile
@@ -12,3 +12,4 @@ endif
obj-$(CONFIG_NET_DSA_MICROCHIP_KSZ9477_I2C) += ksz9477_i2c.o
obj-$(CONFIG_NET_DSA_MICROCHIP_KSZ_SPI) += ksz_spi.o
obj-$(CONFIG_NET_DSA_MICROCHIP_KSZ8863_SMI) += ksz8863_smi.o
+obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X) += lan9645x/
diff --git a/drivers/net/dsa/microchip/lan9645x/Kconfig b/drivers/net/dsa/microchip/lan9645x/Kconfig
new file mode 100644
index 000000000000..4d9fdf34104e
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/Kconfig
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config NET_DSA_MICROCHIP_LAN9645X
+ tristate "Microchip Lan9645x switch support"
+ depends on NET_DSA
+ select NET_DSA_TAG_LAN9645X
+ help
+ This driver adds DSA support for Microchip Lan9645x switch chips.
+ The lan9645x switch is a multi-port Gigabit AVB/TSN Ethernet Switch
+ with five integrated 10/100/1000Base-T PHYs. In addition to the
+ integrated PHYs, it supports up to 2 RGMII/RMII, up to 2
+ BASE-X/SERDES/2.5GBASE-X and one Quad-SGMII/Quad-USGMII interfaces.
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
new file mode 100644
index 000000000000..7cc0ae0ada40
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X) += mchp-lan9645x.o
+
+mchp-lan9645x-objs := \
+ lan9645x_main.o \
+ lan9645x_npi.o \
+ lan9645x_phylink.o \
+ lan9645x_port.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
new file mode 100644
index 000000000000..3e68542ba81d
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -0,0 +1,406 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/platform_device.h>
+
+#include "lan9645x_main.h"
+
+static const char *lan9645x_resource_names[NUM_TARGETS + 1] = {
+ [TARGET_GCB] = "gcb",
+ [TARGET_QS] = "qs",
+ [TARGET_CHIP_TOP] = "chip_top",
+ [TARGET_REW] = "rew",
+ [TARGET_SYS] = "sys",
+ [TARGET_HSIO] = "hsio",
+ [TARGET_DEV] = "dev",
+ [TARGET_DEV + 1] = "dev1",
+ [TARGET_DEV + 2] = "dev2",
+ [TARGET_DEV + 3] = "dev3",
+ [TARGET_DEV + 4] = "dev4",
+ [TARGET_DEV + 5] = "dev5",
+ [TARGET_DEV + 6] = "dev6",
+ [TARGET_DEV + 7] = "dev7",
+ [TARGET_DEV + 8] = "dev8",
+ [TARGET_QSYS] = "qsys",
+ [TARGET_AFI] = "afi",
+ [TARGET_ANA] = "ana",
+ [NUM_TARGETS] = NULL,
+};
+
+static int lan9645x_tag_npi_setup(struct dsa_switch *ds)
+{
+ struct dsa_port *dp, *first_cpu_dp = NULL;
+ struct lan9645x *lan9645x = ds->priv;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ if (first_cpu_dp && dp->cpu_dp != first_cpu_dp) {
+ dev_err(ds->dev, "Multiple NPI ports not supported\n");
+ return -EINVAL;
+ }
+
+ first_cpu_dp = dp->cpu_dp;
+ }
+
+ if (!first_cpu_dp)
+ return -EINVAL;
+
+ lan9645x_npi_port_init(lan9645x, first_cpu_dp);
+
+ return 0;
+}
+
+static enum dsa_tag_protocol lan9645x_get_tag_protocol(struct dsa_switch *ds,
+ int port,
+ enum dsa_tag_protocol tp)
+{
+ return DSA_TAG_PROTO_LAN9645X;
+}
+
+static void lan9645x_teardown(struct dsa_switch *ds)
+{
+ struct lan9645x *lan9645x = ds->priv;
+
+ lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
+}
+
+static int lan9645x_change_mtu(struct dsa_switch *ds, int port, int new_mtu)
+{
+ return lan9645x_port_set_maxlen(ds->priv, port, new_mtu);
+}
+
+static int lan9645x_get_max_mtu(struct dsa_switch *ds, int port)
+{
+ struct lan9645x *lan9645x = ds->priv;
+ int max_mtu;
+
+ /* Actual MAC max MTU is around 16KB. We set 10000 - overhead which
+ * should be sufficient for all jumbo frames. Larger frames can cause
+ * problems especially with flow control, since we only have 160K queue
+ * buffer.
+ */
+ max_mtu = 10000 - ETH_HLEN - ETH_FCS_LEN;
+
+ if (port == lan9645x->npi) {
+ max_mtu -= LAN9645X_IFH_LEN;
+ max_mtu -= LAN9645X_LONG_PREFIX_LEN;
+ }
+
+ return max_mtu;
+}
+
+static int lan9645x_reset_switch(struct lan9645x *lan9645x)
+{
+ int val = 0;
+ int err;
+
+ lan_wr(SYS_RESET_CFG_CORE_ENA_SET(0), lan9645x, SYS_RESET_CFG);
+ lan_wr(SYS_RAM_INIT_RAM_INIT_SET(1), lan9645x, SYS_RAM_INIT);
+ err = lan9645x_rd_poll_timeout(lan9645x, SYS_RAM_INIT, val,
+ SYS_RAM_INIT_RAM_INIT_GET(val) == 0);
+ if (err) {
+ dev_err(lan9645x->dev, "Failed to init chip RAM.\n");
+ return err;
+ }
+ lan_wr(SYS_RESET_CFG_CORE_ENA_SET(1), lan9645x, SYS_RESET_CFG);
+
+ return 0;
+}
+
+static int lan9645x_setup(struct dsa_switch *ds)
+{
+ struct lan9645x *lan9645x = ds->priv;
+ struct device *dev = lan9645x->dev;
+ u32 front_ports;
+ int err;
+
+ lan9645x->num_phys_ports = ds->num_ports;
+ front_ports = dsa_user_ports(ds);
+
+ err = lan9645x_reset_switch(lan9645x);
+ if (err)
+ return err;
+
+ lan9645x->ports = devm_kcalloc(lan9645x->dev, lan9645x->num_phys_ports,
+ sizeof(struct lan9645x_port *),
+ GFP_KERNEL);
+ if (!lan9645x->ports)
+ return -ENOMEM;
+
+ for (int port = 0; port < lan9645x->num_phys_ports; port++) {
+ struct lan9645x_port *p;
+
+ p = devm_kzalloc(lan9645x->dev, sizeof(*p), GFP_KERNEL);
+ if (!p)
+ return -ENOMEM;
+
+ p->lan9645x = lan9645x;
+ p->chip_port = port;
+ lan9645x->ports[port] = p;
+ }
+
+ err = lan9645x_tag_npi_setup(ds);
+ if (err) {
+ dev_err(dev, "Failed to setup NPI port.\n");
+ return err;
+ }
+
+ /* Link Aggregation Mode: NETDEV_LAG_HASH_L2 */
+ lan_wr(ANA_AGGR_CFG_AC_SMAC_ENA |
+ ANA_AGGR_CFG_AC_DMAC_ENA,
+ lan9645x, ANA_AGGR_CFG);
+
+ /* Flush queues */
+ lan_wr(GENMASK(1, 0), lan9645x, QS_XTR_FLUSH);
+
+ /* Allow to drain */
+ usleep_range(1000, 2000);
+
+ /* All Queues normal */
+ lan_wr(0x0, lan9645x, QS_XTR_FLUSH);
+
+ /* Set MAC age time to default value, the entry is aged after
+ * 2 * AGE_PERIOD
+ */
+ lan_wr(ANA_AUTOAGE_AGE_PERIOD_SET(BR_DEFAULT_AGEING_TIME / 2 / HZ),
+ lan9645x, ANA_AUTOAGE);
+
+ /* Disable learning for frames discarded by VLAN ingress filtering */
+ lan_rmw(ANA_ADVLEARN_VLAN_CHK_SET(1),
+ ANA_ADVLEARN_VLAN_CHK,
+ lan9645x, ANA_ADVLEARN);
+
+ /* Queue system frame ageing. We target 2s ageing.
+ *
+ * Register unit is 1024 cycles.
+ *
+ * ASIC: 165.625 Mhz ~ 6.0377 ns period
+ *
+ * 1024 * 6.0377 ns =~ 6182 ns
+ * val = 2000000000ns / 6182ns
+ */
+ lan_wr(SYS_FRM_AGING_AGE_TX_ENA_SET(1) |
+ SYS_FRM_AGING_MAX_AGE_SET((2000000000 / 6182)),
+ lan9645x, SYS_FRM_AGING);
+
+ /* Setup flooding PGIDs for IPv4/IPv6 multicast. Control and dataplane
+ * use the same masks. Control frames are redirected to CPU, and
+ * the network stack is responsible for forwarding these.
+ * The dataplane is forwarding according to the offloaded MDB entries.
+ */
+ lan_wr(ANA_FLOODING_IPMC_FLD_MC4_DATA_SET(PGID_MCIPV4) |
+ ANA_FLOODING_IPMC_FLD_MC4_CTRL_SET(PGID_MC) |
+ ANA_FLOODING_IPMC_FLD_MC6_DATA_SET(PGID_MCIPV6) |
+ ANA_FLOODING_IPMC_FLD_MC6_CTRL_SET(PGID_MC),
+ lan9645x, ANA_FLOODING_IPMC);
+
+ /* There are 8 priorities */
+ for (int prio = 0; prio < 8; ++prio)
+ lan_wr(ANA_FLOODING_FLD_MULTICAST_SET(PGID_MC) |
+ ANA_FLOODING_FLD_UNICAST_SET(PGID_UC) |
+ ANA_FLOODING_FLD_BROADCAST_SET(PGID_BC),
+ lan9645x, ANA_FLOODING(prio));
+
+ /* Allow VLAN table to control whether cpu copy from the pgid table is
+ * enabled.
+ */
+ for (int i = 0; i < PGID_ENTRIES; ++i)
+ lan_wr(ANA_PGID_CFG_OBEY_VLAN_SET(1),
+ lan9645x, ANA_PGID_CFG(i));
+
+ /* Disable bridging by default */
+ for (int p = 0; p < lan9645x->num_phys_ports; p++) {
+ lan_wr(0, lan9645x, ANA_PGID(PGID_SRC + p));
+
+ /* Do not forward BPDU frames to the front ports and copy them
+ * to CPU
+ */
+ lan_wr(ANA_CPU_FWD_BPDU_CFG_BPDU_REDIR_ENA,
+ lan9645x, ANA_CPU_FWD_BPDU_CFG(p));
+ }
+
+ /* Set source buffer size for each priority and port to ~1700 bytes */
+ for (int i = 0; i <= QSYS_Q_RSRV; ++i) {
+ lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(i));
+ lan_wr(QS_SRC_BUF_RSV / 64, lan9645x, QSYS_RES_CFG(512 + i));
+ }
+
+ lan9645x_port_cpu_init(lan9645x);
+
+ /* Multicast to all front ports */
+ lan_wr(front_ports, lan9645x, ANA_PGID(PGID_MC));
+
+ /* IP multicast to all front ports */
+ lan_wr(front_ports, lan9645x, ANA_PGID(PGID_MCIPV4));
+ lan_wr(front_ports, lan9645x, ANA_PGID(PGID_MCIPV6));
+
+ /* Unicast to all front ports */
+ lan_wr(front_ports, lan9645x, ANA_PGID(PGID_UC));
+
+ /* Broadcast to cpu and all front ports */
+ lan_wr(BIT(CPU_PORT) | front_ports, lan9645x, ANA_PGID(PGID_BC));
+
+ lan9645x_port_set_tail_drop_wm(lan9645x);
+
+ ds->mtu_enforcement_ingress = true;
+ ds->assisted_learning_on_cpu_port = true;
+ ds->fdb_isolation = true;
+
+ dev_info(lan9645x->dev,
+ "SKU features: tsn_dis=%d hsr_dis=%d max_ports=%d\n",
+ lan9645x->tsn_dis, lan9645x->dd_dis,
+ lan9645x->num_phys_ports - lan9645x->num_port_dis);
+
+ return 0;
+}
+
+static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config)
+{
+ lan9645x_phylink_get_caps(ds->priv, port, config);
+}
+
+static const struct dsa_switch_ops lan9645x_switch_ops = {
+ .get_tag_protocol = lan9645x_get_tag_protocol,
+
+ .setup = lan9645x_setup,
+ .teardown = lan9645x_teardown,
+ .port_setup = lan9645x_port_setup,
+
+ /* Phylink integration */
+ .phylink_get_caps = lan9645x_port_phylink_get_caps,
+
+ /* MTU */
+ .port_change_mtu = lan9645x_change_mtu,
+ .port_max_mtu = lan9645x_get_max_mtu,
+};
+
+static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
+{
+ const char *resource_name;
+ struct regmap *tgt_map;
+
+ for (int i = 0; i < NUM_TARGETS; i++) {
+ resource_name = lan9645x_resource_names[i];
+ if (!resource_name)
+ continue;
+
+ tgt_map = dev_get_regmap(lan9645x->dev->parent, resource_name);
+ if (IS_ERR_OR_NULL(tgt_map)) {
+ dev_err(lan9645x->dev, "Failed to get regmap=%d\n", i);
+ return -ENODEV;
+ }
+
+ lan9645x->rmap[i] = tgt_map;
+ }
+
+ return 0;
+}
+
+static void lan9645x_set_feat_dis(struct lan9645x *lan9645x)
+{
+ u32 feat_dis;
+
+ /* The features which can be physically disabled on some SKUs are:
+ * 1) Number of ports can be 5, 7 or 9. Any ports can be used, the chip
+ * tracks how many are active.
+ * 2) HSR/PRP. The duplicate discard table can be disabled.
+ * 3) TAS, frame preemption and PSFP can be disabled.
+ */
+ feat_dis = lan_rd(lan9645x, GCB_FEAT_DISABLE);
+
+ lan9645x->num_port_dis =
+ GCB_FEAT_DISABLE_FEAT_NUM_PORTS_DIS_GET(feat_dis);
+ lan9645x->dd_dis = GCB_FEAT_DISABLE_FEAT_DD_DIS_GET(feat_dis);
+ lan9645x->tsn_dis = GCB_FEAT_DISABLE_FEAT_TSN_DIS_GET(feat_dis);
+}
+
+static int lan9645x_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct lan9645x *lan9645x;
+ struct dsa_switch *ds;
+ int err = 0;
+
+ lan9645x = devm_kzalloc(dev, sizeof(*lan9645x), GFP_KERNEL);
+ if (!lan9645x)
+ return dev_err_probe(dev, -ENOMEM,
+ "Failed to allocate LAN9645X");
+
+ dev_set_drvdata(dev, lan9645x);
+ lan9645x->dev = dev;
+
+ err = lan9645x_request_target_regmaps(lan9645x);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to request regmaps");
+
+ ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
+ if (!ds)
+ return dev_err_probe(dev, -ENOMEM,
+ "Failed to allocate DSA switch");
+
+ ds->dev = dev;
+ ds->num_ports = NUM_PHYS_PORTS;
+ ds->num_tx_queues = NUM_PRIO_QUEUES;
+ ds->dscp_prio_mapping_is_global = true;
+
+ ds->ops = &lan9645x_switch_ops;
+ ds->phylink_mac_ops = &lan9645x_phylink_mac_ops;
+ ds->priv = lan9645x;
+
+ lan9645x->ds = ds;
+ lan9645x->shared_queue_sz = LAN9645X_BUFFER_MEMORY;
+
+ lan9645x_set_feat_dis(lan9645x);
+
+ err = dsa_register_switch(ds);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to register DSA switch");
+
+ return 0;
+}
+
+static void lan9645x_remove(struct platform_device *pdev)
+{
+ struct lan9645x *lan9645x = dev_get_drvdata(&pdev->dev);
+
+ if (!lan9645x)
+ return;
+
+ /* Calls lan9645x DSA .teardown */
+ dsa_unregister_switch(lan9645x->ds);
+ dev_set_drvdata(&pdev->dev, NULL);
+}
+
+static void lan9645x_shutdown(struct platform_device *pdev)
+{
+ struct lan9645x *lan9645x = dev_get_drvdata(&pdev->dev);
+
+ if (!lan9645x)
+ return;
+
+ dsa_switch_shutdown(lan9645x->ds);
+
+ dev_set_drvdata(&pdev->dev, NULL);
+}
+
+static const struct of_device_id lan9645x_switch_of_match[] = {
+ { .compatible = "microchip,lan96455s-switch" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, lan9645x_switch_of_match);
+
+static struct platform_driver lan9645x_switch_driver = {
+ .driver = {
+ .name = "lan96455s-switch",
+ .of_match_table = lan9645x_switch_of_match,
+ },
+ .probe = lan9645x_probe,
+ .remove = lan9645x_remove,
+ .shutdown = lan9645x_shutdown,
+};
+module_platform_driver(lan9645x_switch_driver);
+
+MODULE_DESCRIPTION("Lan9645x Switch Driver");
+MODULE_AUTHOR("Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
new file mode 100644
index 000000000000..d8bdcb8a92ed
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -0,0 +1,334 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#ifndef __LAN9645X_MAIN_H__
+#define __LAN9645X_MAIN_H__
+
+#include <linux/dsa/lan9645x.h>
+#include <linux/if_bridge.h>
+#include <linux/regmap.h>
+#include <net/dsa.h>
+
+#include "lan9645x_regs.h"
+
+/* Ports 0-8 are front ports
+ * Ports 9-10 are CPU ports
+ *
+ * CPU ports are logical ports in the chip intended for management. The frame
+ * delivery mechanism can vary: direct register injection/extraction or one can
+ * use a front port as CPU port, called a Node Processor Interface (NPI).
+ *
+ * This is the approach used by LAN9645X.
+ */
+#define NUM_PHYS_PORTS 9
+#define CPU_PORT 9
+#define NUM_PRIO_QUEUES 8
+#define LAN9645X_NUM_TC 8
+
+#define QS_SRC_BUF_RSV 1700
+
+/* Reserved amount for (SRC, PRIO) at index 8*SRC + PRIO
+ * See QSYS:RES_CTRL[*]:RES_CFG description
+ */
+#define QSYS_Q_RSRV 95
+
+#define LAN9645X_ISDX_MAX 128
+#define LAN9645X_ESDX_MAX 128
+#define LAN9645X_SFID_MAX 128
+
+/* Reserved VLAN IDs. */
+#define UNAWARE_PVID 0
+#define HOST_PVID 4095
+#define VLAN_HSR_PRP 4094
+#define VLAN_MAX (VLAN_HSR_PRP - 1)
+
+/* 160KiB / 1.25Mbit */
+#define LAN9645X_BUFFER_MEMORY (160 * 1024)
+
+/* Port Group Identifiers (PGID) are port-masks applied to all frames.
+ * The replicated registers are organized like so in HW:
+ *
+ * 0-63: Destination analysis
+ * 64-79: Aggregation analysis
+ * 80-(80+10-1): Source port analysis
+ *
+ * Destination: By default the first 9 port masks == BIT(port_num). Never change
+ * these except for aggregation. Remaining dst masks are for L2 MC and
+ * flooding. (See FLOODING and FLOODING_IPMC).
+ *
+ * Aggregation: Used to pick a port within an aggregation group. If no
+ * aggregation is configured, these are all-ones.
+ *
+ * Source: Control which ports a given source port can forward to. A frame that
+ * is received on port n, uses mask 80+n as a mask to filter out destination
+ * ports. The default values are that all bits are set except for the index
+ * number (no loopback).
+ *
+ * We reserve destination PGIDs at the end of the range.
+ */
+
+#define PGID_AGGR 64
+#define PGID_SRC 80
+#define PGID_ENTRIES 89
+
+#define PGID_AGGR_NUM (PGID_SRC - PGID_AGGR)
+
+/* General purpose PGIDs. */
+#define PGID_GP_START CPU_PORT
+#define PGID_GP_END PGID_MRP
+
+/* Reserved PGIDs.
+ * PGID_MRP is a blackhole PGID
+ */
+#define PGID_MRP (PGID_AGGR - 7)
+#define PGID_CPU (PGID_AGGR - 6)
+#define PGID_UC (PGID_AGGR - 5)
+#define PGID_BC (PGID_AGGR - 4)
+#define PGID_MC (PGID_AGGR - 3)
+#define PGID_MCIPV4 (PGID_AGGR - 2)
+#define PGID_MCIPV6 (PGID_AGGR - 1)
+
+/* Flooding PGIDS:
+ * PGID_UC
+ * PGID_MC*
+ * PGID_BC
+ */
+
+#define GWM_MULTIPLIER_BIT BIT(8)
+#define LAN9645X_BUFFER_CELL_SZ 64
+
+#define RD_SLEEP_US 3
+#define RD_SLEEPTIMEOUT_US 100000
+#define SLOW_RD_SLEEP_US 1000
+#define SLOW_RD_SLEEPTIMEOUT_US 4000000
+
+#define lan9645x_rd_poll_timeout(_lan9645x, _reg_macro, _val, _cond) \
+ regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
+ lan_rel_addr(_reg_macro), (_val), \
+ (_cond), RD_SLEEP_US, RD_SLEEPTIMEOUT_US)
+
+#define lan9645x_rd_poll_slow(_lan9645x, _reg_macro, _val, _cond) \
+ regmap_read_poll_timeout(lan_rmap((_lan9645x), _reg_macro), \
+ lan_rel_addr(_reg_macro), (_val), \
+ (_cond), SLOW_RD_SLEEP_US, \
+ SLOW_RD_SLEEPTIMEOUT_US)
+
+/* NPI port prefix config encoding
+ *
+ * 0: No CPU extraction header (normal frames)
+ * 1: CPU extraction header without prefix
+ * 2: CPU extraction header with short prefix
+ * 3: CPU extraction header with long prefix
+ */
+enum lan9645x_tag_prefix {
+ LAN9645X_TAG_PREFIX_DISABLED = 0,
+ LAN9645X_TAG_PREFIX_NONE = 1,
+ LAN9645X_TAG_PREFIX_SHORT = 2,
+ LAN9645X_TAG_PREFIX_LONG = 3,
+};
+
+enum {
+ LAN9645X_SPEED_DISABLED = 0,
+ LAN9645X_SPEED_10 = 1,
+ LAN9645X_SPEED_100 = 2,
+ LAN9645X_SPEED_1000 = 3,
+ LAN9645X_SPEED_2500 = 4,
+};
+
+/* Rewriter VLAN port tagging encoding for REW:PORT[0-10]:TAG_CFG.TAG_CFG
+ *
+ * 0: Port tagging disabled.
+ * 1: Tag all frames, except when VID=PORT_VLAN_CFG.PORT_VID or VID=0.
+ * 2: Tag all frames, except when VID=0.
+ * 3: Tag all frames.
+ */
+enum lan9645x_vlan_port_tag {
+ LAN9645X_TAG_DISABLED = 0,
+ LAN9645X_TAG_NO_PVID_NO_UNAWARE = 1,
+ LAN9645X_TAG_NO_UNAWARE = 2,
+ LAN9645X_TAG_ALL = 3,
+};
+
+struct lan9645x {
+ struct device *dev;
+ struct dsa_switch *ds;
+ struct regmap *rmap[NUM_TARGETS];
+
+ int shared_queue_sz;
+
+ /* NPI chip_port */
+ int npi;
+
+ u8 num_phys_ports;
+ struct lan9645x_port **ports;
+
+ int num_port_dis;
+ bool dd_dis;
+ bool tsn_dis;
+};
+
+struct lan9645x_port {
+ struct lan9645x *lan9645x;
+
+ u8 chip_port;
+
+ bool rx_internal_delay;
+ bool tx_internal_delay;
+};
+
+extern const struct phylink_mac_ops lan9645x_phylink_mac_ops;
+
+/* PFC_CFG.FC_LINK_SPEED encoding */
+static inline int lan9645x_speed_fc_enc(int speed)
+{
+ switch (speed) {
+ case LAN9645X_SPEED_10:
+ return 3;
+ case LAN9645X_SPEED_100:
+ return 2;
+ case LAN9645X_SPEED_1000:
+ return 1;
+ case LAN9645X_SPEED_2500:
+ return 0;
+ default:
+ WARN_ON_ONCE(1);
+ return 1;
+ }
+}
+
+/* Watermark encode. See QSYS:RES_CTRL[*]:RES_CFG.WM_HIGH for details.
+ * Returns lowest encoded number which will fit request/ is larger than request.
+ * Or the maximum representable value, if request is too large.
+ */
+static inline u32 lan9645x_wm_enc(u32 value)
+{
+ value = DIV_ROUND_UP(value, LAN9645X_BUFFER_CELL_SZ);
+
+ if (value >= GWM_MULTIPLIER_BIT) {
+ value = DIV_ROUND_UP(value, 16);
+ if (value >= GWM_MULTIPLIER_BIT)
+ value = (GWM_MULTIPLIER_BIT - 1);
+ value |= GWM_MULTIPLIER_BIT;
+ }
+
+ return value;
+}
+
+static inline struct lan9645x_port *lan9645x_to_port(struct lan9645x *lan9645x,
+ int port)
+{
+ if (WARN_ON(!(port >= 0 && port < lan9645x->num_phys_ports)))
+ return NULL;
+
+ return lan9645x->ports[port];
+}
+
+static inline struct regmap *lan_tgt2rmap(struct lan9645x *lan9645x,
+ enum lan9645x_target t, int tinst)
+{
+ return lan9645x->rmap[t + tinst];
+}
+
+static inline u32 __lan_rel_addr(int gbase, int ginst, int gcnt,
+ int gwidth, int raddr, int rinst,
+ int rcnt, int rwidth)
+{
+ WARN_ON(ginst >= gcnt);
+ WARN_ON(rinst >= rcnt);
+ return gbase + ginst * gwidth + raddr + rinst * rwidth;
+}
+
+/* Get register address relative to target instance */
+static inline u32 lan_rel_addr(enum lan9645x_target t, int tinst, int tcnt,
+ int gbase, int ginst, int gcnt, int gwidth,
+ int raddr, int rinst, int rcnt, int rwidth)
+{
+ WARN_ON(tinst >= tcnt);
+ return __lan_rel_addr(gbase, ginst, gcnt, gwidth, raddr, rinst,
+ rcnt, rwidth);
+}
+
+static inline u32 lan_rd(struct lan9645x *lan9645x, enum lan9645x_target t,
+ int tinst, int tcnt, int gbase, int ginst,
+ int gcnt, int gwidth, int raddr, int rinst,
+ int rcnt, int rwidth)
+{
+ u32 addr, val = 0;
+
+ addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+ raddr, rinst, rcnt, rwidth);
+
+ WARN_ON_ONCE(regmap_read(lan_tgt2rmap(lan9645x, t, tinst), addr, &val));
+
+ return val;
+}
+
+static inline int lan_bulk_rd(void *val, size_t val_count,
+ struct lan9645x *lan9645x,
+ enum lan9645x_target t, int tinst, int tcnt,
+ int gbase, int ginst, int gcnt, int gwidth,
+ int raddr, int rinst, int rcnt, int rwidth)
+{
+ u32 addr;
+
+ addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+ raddr, rinst, rcnt, rwidth);
+
+ return regmap_bulk_read(lan_tgt2rmap(lan9645x, t, tinst), addr, val,
+ val_count);
+}
+
+static inline struct regmap *lan_rmap(struct lan9645x *lan9645x,
+ enum lan9645x_target t, int tinst,
+ int tcnt, int gbase, int ginst,
+ int gcnt, int gwidth, int raddr,
+ int rinst, int rcnt, int rwidth)
+{
+ return lan_tgt2rmap(lan9645x, t, tinst);
+}
+
+static inline void lan_wr(u32 val, struct lan9645x *lan9645x,
+ enum lan9645x_target t, int tinst, int tcnt,
+ int gbase, int ginst, int gcnt, int gwidth,
+ int raddr, int rinst, int rcnt, int rwidth)
+{
+ u32 addr;
+
+ addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+ raddr, rinst, rcnt, rwidth);
+
+ WARN_ON_ONCE(regmap_write(lan_tgt2rmap(lan9645x, t, tinst), addr, val));
+}
+
+static inline void lan_rmw(u32 val, u32 mask, struct lan9645x *lan9645x,
+ enum lan9645x_target t, int tinst, int tcnt,
+ int gbase, int ginst, int gcnt, int gwidth,
+ int raddr, int rinst, int rcnt, int rwidth)
+{
+ u32 addr;
+
+ addr = lan_rel_addr(t, tinst, tcnt, gbase, ginst, gcnt, gwidth,
+ raddr, rinst, rcnt, rwidth);
+
+ WARN_ON_ONCE(regmap_update_bits(lan_tgt2rmap(lan9645x, t, tinst),
+ addr, mask, val));
+}
+
+/* lan9645x_npi.c */
+void lan9645x_npi_port_init(struct lan9645x *lan9645x,
+ struct dsa_port *cpu_port);
+void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port);
+
+/* lan9645x_port.c */
+int lan9645x_port_setup(struct dsa_switch *ds, int port);
+void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x);
+int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu);
+void lan9645x_port_cpu_init(struct lan9645x *lan9645x);
+
+/* lan9645x_phylink.c */
+void lan9645x_phylink_get_caps(struct lan9645x *lan9645x, int port,
+ struct phylink_config *c);
+void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port);
+
+#endif /* __LAN9645X_MAIN_H__ */
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
new file mode 100644
index 000000000000..2e8a9bfb43c6
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_npi.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+#include <net/addrconf.h>
+
+#include "lan9645x_main.h"
+
+void lan9645x_npi_port_init(struct lan9645x *lan9645x,
+ struct dsa_port *cpu_port)
+{
+ int port = cpu_port->index;
+ struct lan9645x_port *p;
+
+ p = lan9645x_to_port(lan9645x, port);
+ lan9645x->npi = port;
+
+ dev_dbg(lan9645x->dev, "NPI port=%d\n", port);
+
+ /* Any CPU extraction queue frames, are sent to external CPU on given
+ * port. Never send injected frames back to cpu.
+ */
+ lan_wr(QSYS_EXT_CPU_CFG_EXT_CPUQ_MSK |
+ QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(p->chip_port) |
+ QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA_SET(1) |
+ QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA_SET(1),
+ lan9645x, QSYS_EXT_CPU_CFG);
+
+ /* Configure IFH prefix mode for NPI port. We can not use an injection
+ * prefix, because it requires all frames sent on the port to contain
+ * the prefix. Frames without the prefix would get stuck in the queue
+ * system rendering the port becomes unusable. Since we do not control
+ * what is sent to the NPI port, no prefix is our only option.
+ */
+ lan_rmw(SYS_PORT_MODE_INCL_XTR_HDR_SET(LAN9645X_TAG_PREFIX_LONG) |
+ SYS_PORT_MODE_INCL_INJ_HDR_SET(LAN9645X_TAG_PREFIX_NONE),
+ SYS_PORT_MODE_INCL_XTR_HDR |
+ SYS_PORT_MODE_INCL_INJ_HDR,
+ lan9645x,
+ SYS_PORT_MODE(p->chip_port));
+
+ /* Rewriting and extraction with IFH does not play nice together. A VLAN
+ * tag pushed into the frame by REW will cause 4 bytes at the end of the
+ * extraction header to be overwritten with the top 4 bytes of the DMAC.
+ *
+ * We can not use REW_PORT_CFG_NO_REWRITE=1 as that disabled RTAGD
+ * setting in the IFH
+ */
+ lan_rmw(REW_TAG_CFG_TAG_CFG_SET(LAN9645X_TAG_DISABLED),
+ REW_TAG_CFG_TAG_CFG, lan9645x, REW_TAG_CFG(port));
+
+ /* Make sure frames with src_port=CPU_PORT are not reflected back via
+ * the NPI port. This could happen if a frame is flooded for instance.
+ * The *_CPU_KILL_ENA flags above only have an effect when a frame is
+ * output due to a CPU forwarding decision such as trapping or cpu copy.
+ */
+ lan_rmw(0, BIT(port), lan9645x, ANA_PGID(PGID_SRC + CPU_PORT));
+}
+
+void lan9645x_npi_port_deinit(struct lan9645x *lan9645x, int port)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+
+ lan9645x->npi = -1;
+
+ lan_wr(QSYS_EXT_CPU_CFG_EXT_CPU_PORT_SET(0x1f) |
+ QSYS_EXT_CPU_CFG_EXT_CPU_KILL_ENA_SET(1) |
+ QSYS_EXT_CPU_CFG_INT_CPU_KILL_ENA_SET(1),
+ lan9645x, QSYS_EXT_CPU_CFG);
+
+ lan_rmw(SYS_PORT_MODE_INCL_XTR_HDR_SET(LAN9645X_TAG_PREFIX_DISABLED) |
+ SYS_PORT_MODE_INCL_INJ_HDR_SET(LAN9645X_TAG_PREFIX_DISABLED),
+ SYS_PORT_MODE_INCL_XTR_HDR |
+ SYS_PORT_MODE_INCL_INJ_HDR,
+ lan9645x,
+ SYS_PORT_MODE(p->chip_port));
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
new file mode 100644
index 000000000000..1e55cec9d754
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_phylink.c
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/phy.h>
+#include <linux/phy/phy.h>
+
+#include "lan9645x_main.h"
+
+/* Port 4 or 7 is RGMII_0 and port 8 is RGMII_1 */
+#define RGMII_IDX(port) ((port) == 8 ? 1 : 0)
+
+void lan9645x_phylink_get_caps(struct lan9645x *lan9645x, int port,
+ struct phylink_config *c)
+{
+ c->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
+ MAC_100 | MAC_1000FD | MAC_2500FD;
+
+ switch (port) {
+ case 0 ... 3:
+ __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
+ break;
+ case 4:
+ __set_bit(PHY_INTERFACE_MODE_GMII, c->supported_interfaces);
+ phy_interface_set_rgmii(c->supported_interfaces);
+ break;
+ case 5 ... 6:
+ /* SerDes ports: QSGMII/SGMII/1000BASEX/2500BASEX modes
+ * require PCS support which is not yet implemented.
+ */
+ break;
+ case 7 ... 8:
+ /* QSGMII mode on ports 7-8 requires SerDes PCS support,
+ * which is not yet implemented.
+ */
+ phy_interface_set_rgmii(c->supported_interfaces);
+ break;
+ default:
+ break;
+ }
+}
+
+static void lan9645x_rgmii_set_speed(struct lan9645x *lan9645x, int port,
+ int speed)
+{
+ u8 tx_clk;
+
+ tx_clk = speed == SPEED_1000 ? 1 :
+ speed == SPEED_100 ? 2 :
+ speed == SPEED_10 ? 3 : 0;
+
+ lan_rmw(HSIO_RGMII_CFG_RGMII_RX_RST_SET(0) |
+ HSIO_RGMII_CFG_RGMII_TX_RST_SET(0) |
+ HSIO_RGMII_CFG_TX_CLK_CFG_SET(tx_clk),
+ HSIO_RGMII_CFG_RGMII_RX_RST |
+ HSIO_RGMII_CFG_RGMII_TX_RST |
+ HSIO_RGMII_CFG_TX_CLK_CFG,
+ lan9645x, HSIO_RGMII_CFG(RGMII_IDX(port)));
+}
+
+static void lan9645x_rgmii_dll_config(struct lan9645x_port *p)
+{
+ u32 rx_idx, tx_idx;
+
+ /* DLL register layout:
+ * (N*2): RGMII_N_RX
+ * (N*2)+1: RGMII_N_TX
+ */
+ rx_idx = RGMII_IDX(p->chip_port) * 2;
+ tx_idx = RGMII_IDX(p->chip_port) * 2 + 1;
+
+ /* Enable DLL in RGMII clock paths, deassert DLL reset, and start the
+ * delay tune FSM.
+ */
+ lan_rmw(HSIO_DLL_CFG_DLL_CLK_ENA_SET(1) |
+ HSIO_DLL_CFG_DLL_RST_SET(0) |
+ HSIO_DLL_CFG_DLL_ENA_SET(p->rx_internal_delay) |
+ HSIO_DLL_CFG_DELAY_ENA_SET(p->rx_internal_delay),
+ HSIO_DLL_CFG_DLL_CLK_ENA |
+ HSIO_DLL_CFG_DLL_RST |
+ HSIO_DLL_CFG_DLL_ENA |
+ HSIO_DLL_CFG_DELAY_ENA,
+ p->lan9645x, HSIO_DLL_CFG(rx_idx));
+
+ lan_rmw(HSIO_DLL_CFG_DLL_CLK_ENA_SET(1) |
+ HSIO_DLL_CFG_DLL_RST_SET(0) |
+ HSIO_DLL_CFG_DLL_ENA_SET(p->tx_internal_delay) |
+ HSIO_DLL_CFG_DELAY_ENA_SET(p->tx_internal_delay),
+ HSIO_DLL_CFG_DLL_CLK_ENA |
+ HSIO_DLL_CFG_DLL_RST |
+ HSIO_DLL_CFG_DLL_ENA |
+ HSIO_DLL_CFG_DELAY_ENA,
+ p->lan9645x, HSIO_DLL_CFG(tx_idx));
+}
+
+static struct lan9645x_port *
+lan9645x_phylink_config_to_port(struct phylink_config *config)
+{
+ struct dsa_port *dp = dsa_phylink_to_port(config);
+
+ return lan9645x_to_port(dp->ds->priv, dp->index);
+}
+
+static int lan9645x_phylink_mac_prepare(struct phylink_config *config,
+ unsigned int mode,
+ phy_interface_t iface)
+{
+ struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+ struct lan9645x *lan9645x = p->lan9645x;
+ int port = p->chip_port;
+ u32 mask;
+
+ if (port == 5 || port == 6 || port > 8)
+ return -EINVAL;
+
+ mask = HSIO_HW_CFG_GMII_ENA_SET(BIT(port));
+ lan_rmw(mask, mask, lan9645x, HSIO_HW_CFG);
+
+ if (port == 4 && phy_interface_mode_is_rgmii(iface))
+ lan_rmw(HSIO_HW_CFG_RGMII_0_CFG_SET(1),
+ HSIO_HW_CFG_RGMII_0_CFG,
+ lan9645x, HSIO_HW_CFG);
+
+ return 0;
+}
+
+static void lan9645x_phylink_mac_config(struct phylink_config *config,
+ unsigned int mode,
+ const struct phylink_link_state *state)
+{
+ struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+
+ if (phy_interface_mode_is_rgmii(state->interface))
+ lan9645x_rgmii_dll_config(p);
+}
+
+static bool lan9645x_port_is_cuphy(struct lan9645x *lan9645x, int port,
+ phy_interface_t interface)
+{
+ return port >= 0 && port <= 4 && interface == PHY_INTERFACE_MODE_GMII;
+}
+
+void lan9645x_phylink_port_down(struct lan9645x *lan9645x, int port)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+ u32 val;
+
+ /* Disable MAC frame reception */
+ lan_rmw(DEV_MAC_ENA_CFG_RX_ENA_SET(0),
+ DEV_MAC_ENA_CFG_RX_ENA,
+ lan9645x, DEV_MAC_ENA_CFG(p->chip_port));
+
+ /* Disable traffic being sent to or from switch port */
+ lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(0),
+ QSYS_SW_PORT_MODE_PORT_ENA,
+ lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+
+ /* Disable dequeuing from the egress queues */
+ lan_rmw(QSYS_PORT_MODE_DEQUEUE_DIS_SET(1),
+ QSYS_PORT_MODE_DEQUEUE_DIS,
+ lan9645x, QSYS_PORT_MODE(p->chip_port));
+
+ /* Disable Flowcontrol */
+ lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(0),
+ SYS_PAUSE_CFG_PAUSE_ENA,
+ lan9645x, SYS_PAUSE_CFG(p->chip_port));
+
+ /* Wait a worst case time 8ms (10K jumbo/10Mbit) */
+ usleep_range(8 * USEC_PER_MSEC, 9 * USEC_PER_MSEC);
+
+ /* Disable HDX backpressure. */
+ lan_rmw(SYS_FRONT_PORT_MODE_HDX_MODE_SET(0),
+ SYS_FRONT_PORT_MODE_HDX_MODE,
+ lan9645x, SYS_FRONT_PORT_MODE(p->chip_port));
+
+ /* Flush the queues associated with the port */
+ lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(3),
+ QSYS_SW_PORT_MODE_AGING_MODE,
+ lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+
+ /* Enable dequeuing from the egress queues */
+ lan_rmw(QSYS_PORT_MODE_DEQUEUE_DIS_SET(0),
+ QSYS_PORT_MODE_DEQUEUE_DIS,
+ lan9645x, QSYS_PORT_MODE(p->chip_port));
+
+ /* Wait until flushing is complete */
+ if (lan9645x_rd_poll_slow(lan9645x, QSYS_SW_STATUS(p->chip_port),
+ val, !QSYS_SW_STATUS_EQ_AVAIL_GET(val)))
+ dev_err(lan9645x->dev, "Flush timeout chip port %u\n", port);
+
+ /* Disable MAC tx */
+ lan_rmw(DEV_MAC_ENA_CFG_TX_ENA_SET(0),
+ DEV_MAC_ENA_CFG_TX_ENA,
+ lan9645x, DEV_MAC_ENA_CFG(p->chip_port));
+
+ /* Reset the Port and MAC clock domains */
+ lan_rmw(DEV_CLOCK_CFG_PORT_RST_SET(1),
+ DEV_CLOCK_CFG_PORT_RST,
+ lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+ /* Wait before resetting MAC clock domains. */
+ usleep_range(USEC_PER_MSEC, 2 * USEC_PER_MSEC);
+
+ lan_rmw(DEV_CLOCK_CFG_MAC_TX_RST_SET(1) |
+ DEV_CLOCK_CFG_MAC_RX_RST_SET(1) |
+ DEV_CLOCK_CFG_PORT_RST_SET(1),
+ DEV_CLOCK_CFG_MAC_TX_RST |
+ DEV_CLOCK_CFG_MAC_RX_RST |
+ DEV_CLOCK_CFG_PORT_RST,
+ lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+ /* Clear flushing */
+ lan_rmw(QSYS_SW_PORT_MODE_AGING_MODE_SET(1),
+ QSYS_SW_PORT_MODE_AGING_MODE,
+ lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+}
+
+static void lan9645x_phylink_mac_link_down(struct phylink_config *config,
+ unsigned int link_an_mode,
+ phy_interface_t interface)
+{
+ struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+ struct lan9645x *lan9645x = p->lan9645x;
+
+ lan9645x_phylink_port_down(lan9645x, p->chip_port);
+}
+
+static void lan9645x_phylink_mac_link_up(struct phylink_config *config,
+ struct phy_device *phydev,
+ unsigned int link_an_mode,
+ phy_interface_t interface, int speed,
+ int duplex, bool tx_pause,
+ bool rx_pause)
+{
+ struct lan9645x_port *p = lan9645x_phylink_config_to_port(config);
+ int rx_ifg1, rx_ifg2, tx_ifg, gtx_clk = 0;
+ struct lan9645x *lan9645x = p->lan9645x;
+ int gspeed = LAN9645X_SPEED_DISABLED;
+ int port = p->chip_port;
+ int mode = 0;
+ int fc_spd;
+
+ /* Configure RGMII TX clock for the negotiated speed */
+ if (phy_interface_mode_is_rgmii(interface))
+ lan9645x_rgmii_set_speed(lan9645x, port, speed);
+
+ if (duplex == DUPLEX_FULL) {
+ mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+ tx_ifg = 0x5;
+ rx_ifg2 = 0x2;
+
+ } else {
+ tx_ifg = 0x6;
+ rx_ifg2 = 0x2;
+ }
+
+ switch (speed) {
+ case SPEED_10:
+ rx_ifg1 = 0x2;
+ gspeed = LAN9645X_SPEED_10;
+ break;
+ case SPEED_100:
+ rx_ifg1 = 0x1;
+ gspeed = LAN9645X_SPEED_100;
+ break;
+ case SPEED_1000:
+ gspeed = LAN9645X_SPEED_1000;
+ mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
+ mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+ tx_ifg = 0x6;
+ rx_ifg1 = 0x1;
+ rx_ifg2 = 0x2;
+ gtx_clk = 1;
+ break;
+ case SPEED_2500:
+ gspeed = LAN9645X_SPEED_2500;
+ mode |= DEV_MAC_MODE_CFG_GIGA_MODE_ENA_SET(1);
+ mode |= DEV_MAC_MODE_CFG_FDX_ENA_SET(1);
+ tx_ifg = 0x6;
+ rx_ifg1 = 0x1;
+ rx_ifg2 = 0x2;
+ break;
+ default:
+ dev_err(lan9645x->dev, "Unsupported speed on port %d: %d\n",
+ p->chip_port, speed);
+ return;
+ }
+
+ fc_spd = lan9645x_speed_fc_enc(gspeed);
+
+ lan_rmw(mode,
+ DEV_MAC_MODE_CFG_FDX_ENA |
+ DEV_MAC_MODE_CFG_GIGA_MODE_ENA,
+ lan9645x, DEV_MAC_MODE_CFG(p->chip_port));
+
+ lan_rmw(DEV_MAC_IFG_CFG_TX_IFG_SET(tx_ifg) |
+ DEV_MAC_IFG_CFG_RX_IFG1_SET(rx_ifg1) |
+ DEV_MAC_IFG_CFG_RX_IFG2_SET(rx_ifg2),
+ DEV_MAC_IFG_CFG_TX_IFG |
+ DEV_MAC_IFG_CFG_RX_IFG1 |
+ DEV_MAC_IFG_CFG_RX_IFG2,
+ lan9645x, DEV_MAC_IFG_CFG(p->chip_port));
+
+ if (lan9645x_port_is_cuphy(lan9645x, port, interface)) {
+ lan_rmw(CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA_SET(gtx_clk),
+ CHIP_TOP_CUPHY_PORT_CFG_GTX_CLK_ENA, lan9645x,
+ CHIP_TOP_CUPHY_PORT_CFG(p->chip_port));
+ }
+
+ lan_rmw(SYS_PAUSE_CFG_PAUSE_ENA_SET(1),
+ SYS_PAUSE_CFG_PAUSE_ENA,
+ lan9645x, SYS_PAUSE_CFG(p->chip_port));
+
+ /* Flow control */
+ lan_rmw(SYS_MAC_FC_CFG_FC_LINK_SPEED_SET(fc_spd) |
+ SYS_MAC_FC_CFG_FC_LATENCY_CFG_SET(0x7) |
+ SYS_MAC_FC_CFG_ZERO_PAUSE_ENA_SET(1) |
+ SYS_MAC_FC_CFG_PAUSE_VAL_CFG_SET(0xffff) |
+ SYS_MAC_FC_CFG_RX_FC_ENA_SET(rx_pause ? 1 : 0) |
+ SYS_MAC_FC_CFG_TX_FC_ENA_SET(tx_pause ? 1 : 0),
+ SYS_MAC_FC_CFG_FC_LINK_SPEED |
+ SYS_MAC_FC_CFG_FC_LATENCY_CFG |
+ SYS_MAC_FC_CFG_ZERO_PAUSE_ENA |
+ SYS_MAC_FC_CFG_PAUSE_VAL_CFG |
+ SYS_MAC_FC_CFG_RX_FC_ENA |
+ SYS_MAC_FC_CFG_TX_FC_ENA,
+ lan9645x, SYS_MAC_FC_CFG(p->chip_port));
+
+ /* Enable MAC module */
+ lan_wr(DEV_MAC_ENA_CFG_RX_ENA_SET(1) |
+ DEV_MAC_ENA_CFG_TX_ENA_SET(1),
+ lan9645x, DEV_MAC_ENA_CFG(p->chip_port));
+
+ /* port _must_ be taken out of reset before MAC. */
+ lan_rmw(DEV_CLOCK_CFG_PORT_RST_SET(0),
+ DEV_CLOCK_CFG_PORT_RST,
+ lan9645x, DEV_CLOCK_CFG(p->chip_port));
+
+ /* Take out the clock from reset. Note this write will set all these
+ * fields to zero:
+ *
+ * DEV_CLOCK_CFG[*].MAC_TX_RST
+ * DEV_CLOCK_CFG[*].MAC_RX_RST
+ * DEV_CLOCK_CFG[*].PCS_TX_RST
+ * DEV_CLOCK_CFG[*].PCS_RX_RST
+ * DEV_CLOCK_CFG[*].PORT_RST
+ * DEV_CLOCK_CFG[*].PHY_RST
+ *
+ * Note link_down will assert PORT_RST, MAC_RX_RST and MAC_TX_RST, so
+ * we are effectively taking the mac tx/rx clocks out of reset.
+ *
+ * This linkspeed field has a slightly different encoding from others:
+ *
+ * - 0 is no-link
+ * - 1 is both 2500/1000
+ * - 2 is 100mbit
+ * - 3 is 10mbit
+ *
+ */
+ lan_wr(DEV_CLOCK_CFG_LINK_SPEED_SET(fc_spd == 0 ? 1 : fc_spd),
+ lan9645x,
+ DEV_CLOCK_CFG(p->chip_port));
+
+ /* Core: Enable port for frame transfer */
+ lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(1) |
+ QSYS_SW_PORT_MODE_SCH_NEXT_CFG_SET(1) |
+ QSYS_SW_PORT_MODE_INGRESS_DROP_MODE_SET(1) |
+ QSYS_SW_PORT_MODE_TX_PFC_ENA_SET(0),
+ QSYS_SW_PORT_MODE_PORT_ENA |
+ QSYS_SW_PORT_MODE_SCH_NEXT_CFG |
+ QSYS_SW_PORT_MODE_INGRESS_DROP_MODE |
+ QSYS_SW_PORT_MODE_TX_PFC_ENA,
+ lan9645x, QSYS_SW_PORT_MODE(p->chip_port));
+}
+
+const struct phylink_mac_ops lan9645x_phylink_mac_ops = {
+ .mac_prepare = lan9645x_phylink_mac_prepare,
+ .mac_config = lan9645x_phylink_mac_config,
+ .mac_link_down = lan9645x_phylink_mac_link_down,
+ .mac_link_up = lan9645x_phylink_mac_link_up,
+};
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
new file mode 100644
index 000000000000..394a20ee678f
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/of_net.h>
+
+#include "lan9645x_main.h"
+
+void lan9645x_port_cpu_init(struct lan9645x *lan9645x)
+{
+ /* Map the 8 CPU extraction queues to CPU port 9 (datasheet is wrong) */
+ lan_wr(0, lan9645x, QSYS_CPU_GROUP_MAP);
+
+ /* Set min-spacing of EOF to SOF on injected frames to 0, on cpu device
+ * 1. This is required when injecting with IFH.
+ * Default values emulates delay of std preamble/IFG setting on a front
+ * port.
+ */
+ lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
+ QS_INJ_CTRL_GAP_SIZE,
+ lan9645x, QS_INJ_CTRL(1));
+
+ /* Injection: Mode: manual injection | Byte_swap */
+ lan_wr(QS_INJ_GRP_CFG_MODE_SET(1) |
+ QS_INJ_GRP_CFG_BYTE_SWAP_SET(1),
+ lan9645x, QS_INJ_GRP_CFG(1));
+
+ lan_rmw(QS_INJ_CTRL_GAP_SIZE_SET(0),
+ QS_INJ_CTRL_GAP_SIZE,
+ lan9645x, QS_INJ_CTRL(1));
+
+ lan_wr(SYS_PORT_MODE_INCL_INJ_HDR_SET(1),
+ lan9645x, SYS_PORT_MODE(CPU_PORT + 1));
+
+ /* The CPU will only use its reserved buffer in the shared queue system
+ * and none of the shared buffer space, therefore we disable resource
+ * sharing in egress direction. We must not disable resource sharing in
+ * the ingress direction, because some traffic test scenarios require
+ * loads of buffer memory for frames initiated by the CPU.
+ */
+ lan_rmw(QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(BIT(CPU_PORT)),
+ QSYS_EGR_NO_SHARING_EGR_NO_SHARING_SET(BIT(CPU_PORT)),
+ lan9645x, QSYS_EGR_NO_SHARING);
+
+ /* The CPU should also discard frames forwarded to it if it has run
+ * out of the reserved buffer space. Otherwise they will be held back
+ * in the ingress queues with potential head-of-line blocking effects.
+ */
+ lan_rmw(QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(BIT(CPU_PORT)),
+ QSYS_EGR_DROP_MODE_EGRESS_DROP_MODE_SET(BIT(CPU_PORT)),
+ lan9645x, QSYS_EGR_DROP_MODE);
+
+ lan_wr(BIT(CPU_PORT), lan9645x, ANA_PGID(PGID_CPU));
+
+ lan_rmw(ANA_PORT_CFG_PORTID_VAL_SET(CPU_PORT) |
+ ANA_PORT_CFG_RECV_ENA_SET(1),
+ ANA_PORT_CFG_PORTID_VAL |
+ ANA_PORT_CFG_RECV_ENA, lan9645x,
+ ANA_PORT_CFG(CPU_PORT));
+
+ /* Enable switching to/from cpu port. Keep default aging-mode. */
+ lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(1) |
+ QSYS_SW_PORT_MODE_SCH_NEXT_CFG_SET(1) |
+ QSYS_SW_PORT_MODE_INGRESS_DROP_MODE_SET(1),
+ QSYS_SW_PORT_MODE_PORT_ENA |
+ QSYS_SW_PORT_MODE_SCH_NEXT_CFG |
+ QSYS_SW_PORT_MODE_INGRESS_DROP_MODE,
+ lan9645x, QSYS_SW_PORT_MODE(CPU_PORT));
+
+ /* Transmit cpu frames as received without any tagging, timing or other
+ * updates. This does not affect CPU-over-NPI, only manual extraction.
+ * On the NPI port we need NO_REWRITE=0 for HSR/PRP.
+ */
+ lan_wr(REW_PORT_CFG_NO_REWRITE_SET(1),
+ lan9645x, REW_PORT_CFG(CPU_PORT));
+}
+
+void lan9645x_port_set_tail_drop_wm(struct lan9645x *lan9645x)
+{
+ int shared_per_port;
+ struct dsa_port *dp;
+
+ /* Configure tail dropping watermark */
+ shared_per_port =
+ lan9645x->shared_queue_sz / (lan9645x->num_phys_ports + 1);
+
+ /* The total memory size is divided by number of front ports plus CPU
+ * port.
+ */
+ dsa_switch_for_each_available_port(dp, lan9645x->ds)
+ lan_wr(lan9645x_wm_enc(shared_per_port), lan9645x,
+ SYS_ATOP(dp->index));
+
+ /* Tail dropping active based only on per port ATOP wm */
+ lan_wr(lan9645x_wm_enc(lan9645x->shared_queue_sz), lan9645x,
+ SYS_ATOP_TOT_CFG);
+}
+
+/* VLAN tag overhead is handled by DEV_MAC_TAGS_CFG */
+int lan9645x_port_set_maxlen(struct lan9645x *lan9645x, int port, size_t sdu)
+{
+ struct lan9645x_port *p = lan9645x_to_port(lan9645x, port);
+ int maxlen = sdu + ETH_HLEN + ETH_FCS_LEN;
+
+ if (port == lan9645x->npi) {
+ maxlen += LAN9645X_IFH_LEN;
+ maxlen += LAN9645X_LONG_PREFIX_LEN;
+ }
+
+ lan_wr(DEV_MAC_MAXLEN_CFG_MAX_LEN_SET(maxlen), lan9645x,
+ DEV_MAC_MAXLEN_CFG(p->chip_port));
+
+ /* Set Pause WM hysteresis */
+ lan_rmw(SYS_PAUSE_CFG_PAUSE_STOP_SET(lan9645x_wm_enc(4 * maxlen)) |
+ SYS_PAUSE_CFG_PAUSE_START_SET(lan9645x_wm_enc(6 * maxlen)),
+ SYS_PAUSE_CFG_PAUSE_START |
+ SYS_PAUSE_CFG_PAUSE_STOP,
+ lan9645x,
+ SYS_PAUSE_CFG(p->chip_port));
+
+ return 0;
+}
+
+int lan9645x_port_setup(struct dsa_switch *ds, int port)
+{
+ struct dsa_port *dp = dsa_to_port(ds, port);
+ struct lan9645x *lan9645x = ds->priv;
+ struct lan9645x_port *p;
+
+ p = lan9645x_to_port(lan9645x, port);
+
+ if (dp->dn) {
+ u32 val;
+
+ if (!of_property_read_u32(dp->dn, "rx-internal-delay-ps", &val))
+ p->rx_internal_delay = val > 0;
+
+ if (!of_property_read_u32(dp->dn, "tx-internal-delay-ps", &val))
+ p->tx_internal_delay = val > 0;
+ }
+
+ /* Disable learning on port */
+ lan_rmw(ANA_PORT_CFG_LEARN_ENA_SET(0),
+ ANA_PORT_CFG_LEARN_ENA,
+ lan9645x, ANA_PORT_CFG(p->chip_port));
+
+ lan9645x_port_set_maxlen(lan9645x, port, ETH_DATA_LEN);
+
+ /* Load HDX backoff seed (fixed per-port, one-shot strobe) */
+ lan_rmw(DEV_MAC_HDX_CFG_SEED_SET(p->chip_port) |
+ DEV_MAC_HDX_CFG_SEED_LOAD_SET(1),
+ DEV_MAC_HDX_CFG_SEED |
+ DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
+ DEV_MAC_HDX_CFG(p->chip_port));
+
+ lan_rmw(DEV_MAC_HDX_CFG_SEED_LOAD_SET(0),
+ DEV_MAC_HDX_CFG_SEED_LOAD, lan9645x,
+ DEV_MAC_HDX_CFG(p->chip_port));
+
+ /* Set SMAC of Pause frame (00:00:00:00:00:00) */
+ lan_wr(0, lan9645x, DEV_FC_MAC_LOW_CFG(p->chip_port));
+ lan_wr(0, lan9645x, DEV_FC_MAC_HIGH_CFG(p->chip_port));
+
+ lan9645x_phylink_port_down(lan9645x, port);
+
+ /* Drop frames with multicast source address */
+ lan_rmw(ANA_DROP_CFG_DROP_MC_SMAC_ENA_SET(1),
+ ANA_DROP_CFG_DROP_MC_SMAC_ENA, lan9645x,
+ ANA_DROP_CFG(p->chip_port));
+
+ lan_rmw(DEV_MAC_TAGS_CFG_VLAN_AWR_ENA_SET(1) |
+ DEV_MAC_TAGS_CFG_PB_ENA_SET(1) |
+ DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA_SET(1) |
+ DEV_MAC_TAGS_CFG_TAG_ID_SET(ETH_P_8021AD),
+ DEV_MAC_TAGS_CFG_VLAN_AWR_ENA |
+ DEV_MAC_TAGS_CFG_PB_ENA |
+ DEV_MAC_TAGS_CFG_VLAN_LEN_AWR_ENA |
+ DEV_MAC_TAGS_CFG_TAG_ID,
+ lan9645x, DEV_MAC_TAGS_CFG(p->chip_port));
+
+ /* Enable receiving frames on the port, and activate auto-learning of
+ * MAC addresses. LEARNAUTO is ignored when LEARN_ENA=0.
+ */
+ lan_rmw(ANA_PORT_CFG_LEARNAUTO_SET(1) |
+ ANA_PORT_CFG_RECV_ENA_SET(1) |
+ ANA_PORT_CFG_PORTID_VAL_SET(p->chip_port),
+ ANA_PORT_CFG_LEARNAUTO |
+ ANA_PORT_CFG_RECV_ENA |
+ ANA_PORT_CFG_PORTID_VAL,
+ lan9645x, ANA_PORT_CFG(p->chip_port));
+
+ return 0;
+}
--
2.52.0
^ permalink raw reply related
* [PATCH net-next v3 8/9] net: dsa: lan9645x: add mdb management
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
In-Reply-To: <20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com>
Add support for dsa mdb callbacks. L2 multicast and IP multicast is
handled differently. IP multicast stores the port group mask inline in
the mac table. L2 multicast points to a PGID index, which encodes the
port group mask.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- avoid mdb add/del dealloc when mac table writes fail
- dealloc mdb entries on deinit
Changes in v2:
- New file: selftests required implementation of the mdb callbacks.
---
drivers/net/dsa/microchip/lan9645x/Makefile | 1 +
drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c | 14 +
drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 61 ++++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 21 ++
drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c | 383 +++++++++++++++++++++
drivers/net/dsa/microchip/lan9645x/lan9645x_port.c | 8 +
include/linux/dsa/lan9645x.h | 11 +
net/dsa/tag_lan9645x.c | 19 +-
8 files changed, 516 insertions(+), 2 deletions(-)
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
index 70815edca5b9..2413d11fe849 100644
--- a/drivers/net/dsa/microchip/lan9645x/Makefile
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -4,6 +4,7 @@ obj-$(CONFIG_NET_DSA_MICROCHIP_LAN9645X) += mchp-lan9645x.o
mchp-lan9645x-objs := \
lan9645x_mac.o \
lan9645x_main.o \
+ lan9645x_mdb.o \
lan9645x_npi.o \
lan9645x_phylink.o \
lan9645x_port.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c
index f516979225ae..0bd97cc8e42c 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_mac.c
@@ -215,6 +215,20 @@ int lan9645x_mact_learn(struct lan9645x *lan9645x, int port,
return err;
}
+int lan9645x_mact_learn_cpu_copy(struct lan9645x *lan9645x, int port,
+ const unsigned char *addr, u16 vid,
+ enum macaccess_entry_type type, bool cpu_copy)
+{
+ int err;
+
+ mutex_lock(&lan9645x->mact_lock);
+ err = __lan9645x_mact_learn_cpu_copy(lan9645x, port, addr, vid, type,
+ cpu_copy);
+ mutex_unlock(&lan9645x->mact_lock);
+
+ return err;
+}
+
int lan9645x_mact_flush(struct lan9645x *lan9645x, int port)
{
int err;
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index 764f4d6c0571..8f63729ff55d 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -72,6 +72,7 @@ static void lan9645x_teardown(struct dsa_switch *ds)
destroy_workqueue(lan9645x->owq);
lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
lan9645x_mac_deinit(lan9645x);
+ lan9645x_mdb_deinit(lan9645x);
mutex_destroy(&lan9645x->fwd_domain_lock);
}
@@ -161,6 +162,7 @@ static int lan9645x_setup(struct dsa_switch *ds)
if (err)
return err;
lan9645x_mac_init(lan9645x);
+ lan9645x_mdb_init(lan9645x);
/* Link Aggregation Mode: NETDEV_LAG_HASH_L2 */
lan_wr(ANA_AGGR_CFG_AC_SMAC_ENA |
@@ -680,6 +682,61 @@ static int lan9645x_fdb_del(struct dsa_switch *ds, int port,
return err;
}
+static int lan9645x_mdb_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ struct dsa_db db)
+{
+ struct net_device *bridge_dev = lan9645x_db2bridge(db);
+ struct lan9645x *lan9645x = ds->priv;
+
+ dev_dbg(lan9645x->dev, "port=%d addr=%pM vid=%u\n", port, mdb->addr,
+ mdb->vid);
+
+ if (IS_ERR(bridge_dev))
+ return PTR_ERR(bridge_dev);
+
+ if (dsa_is_cpu_port(ds, port) && !bridge_dev &&
+ dsa_mdb_present_in_other_db(ds, port, mdb, db))
+ return 0;
+
+ if (port == lan9645x->npi)
+ port = CPU_PORT;
+
+ return lan9645x_mdb_port_add(lan9645x, port, mdb, bridge_dev);
+}
+
+static int lan9645x_mdb_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ struct dsa_db db)
+{
+ struct net_device *bridge_dev = lan9645x_db2bridge(db);
+ struct lan9645x *lan9645x = ds->priv;
+ int err;
+
+ dev_dbg(lan9645x->dev, "port=%d addr=%pM vid=%u\n", port, mdb->addr,
+ mdb->vid);
+
+ if (IS_ERR(bridge_dev))
+ return PTR_ERR(bridge_dev);
+
+ if (dsa_is_cpu_port(ds, port) && !bridge_dev &&
+ dsa_mdb_present_in_other_db(ds, port, mdb, db))
+ return 0;
+
+ if (port == lan9645x->npi)
+ port = CPU_PORT;
+
+ err = lan9645x_mdb_port_del(lan9645x, port, mdb, bridge_dev);
+ if (err == -ENOENT) {
+ dev_dbg(lan9645x->dev,
+ "mdb not found port=%d addr=%pM vid=%u\n", port,
+ mdb->addr, mdb->vid);
+ return 0;
+ }
+
+ return err;
+}
+
static const struct dsa_switch_ops lan9645x_switch_ops = {
.get_tag_protocol = lan9645x_get_tag_protocol,
@@ -713,6 +770,10 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
.port_fdb_dump = lan9645x_fdb_dump,
.port_fdb_add = lan9645x_fdb_add,
.port_fdb_del = lan9645x_fdb_del,
+
+ /* Multicast database */
+ .port_mdb_add = lan9645x_mdb_add,
+ .port_mdb_del = lan9645x_mdb_del,
};
static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index a5e64218d783..1c3c9899ed07 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -218,6 +218,14 @@ struct lan9645x {
/* VLAN entries */
struct lan9645x_vlan vlans[VLAN_N_VID];
+ /* Multicast Forwarding Database */
+ struct list_head mdb_entries;
+ struct list_head pgid_entries;
+ /* lock for mdb_entries and pgid_entries. Must be taken before mact_lock
+ * if both are taken.
+ */
+ struct mutex mdb_lock;
+
int num_port_dis;
bool dd_dis;
bool tsn_dis;
@@ -422,5 +430,18 @@ int lan9645x_mact_entry_del(struct lan9645x *lan9645x, int pgid,
const unsigned char *mac, u16 vid);
int lan9645x_mact_entry_add(struct lan9645x *lan9645x, int pgid,
const unsigned char *mac, u16 vid);
+int lan9645x_mact_learn_cpu_copy(struct lan9645x *lan9645x, int port,
+ const unsigned char *addr, u16 vid,
+ enum macaccess_entry_type type, bool cpu_copy);
+
+/* Multicast Database lan9645x_mdb.c */
+int lan9645x_mdb_port_add(struct lan9645x *lan9645x, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ struct net_device *bridge);
+int lan9645x_mdb_port_del(struct lan9645x *lan9645x, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ struct net_device *bridge);
+void lan9645x_mdb_init(struct lan9645x *lan9645x);
+void lan9645x_mdb_deinit(struct lan9645x *lan9645x);
#endif /* __LAN9645X_MAIN_H__ */
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c
new file mode 100644
index 000000000000..24e02aa1fd6f
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_mdb.c
@@ -0,0 +1,383 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include "lan9645x_main.h"
+
+/* HW ignores dest_idx for IPv4/IPv6 types, so we use this dummy index */
+#define IP_ENTRY_PGID 0
+#define PGID_INDEX(pgid) ((pgid) ? (pgid)->index : IP_ENTRY_PGID)
+
+struct lan9645x_pgid_entry {
+ struct list_head list;
+ int index;
+ refcount_t refcount;
+ u16 ports;
+};
+
+struct lan9645x_mdb_entry {
+ struct list_head list;
+ unsigned char mac[ETH_ALEN];
+ u16 vid;
+ u16 ports;
+ struct lan9645x_pgid_entry *pgid;
+};
+
+void lan9645x_mdb_init(struct lan9645x *lan9645x)
+{
+ INIT_LIST_HEAD(&lan9645x->mdb_entries);
+ INIT_LIST_HEAD(&lan9645x->pgid_entries);
+ mutex_init(&lan9645x->mdb_lock);
+
+ /* Use CPU queues to communicate frame classification to the CPU */
+ lan_rmw(ANA_CPUQ_CFG_CPUQ_IGMP_SET(LAN9645X_CPUQ_IGMP) |
+ ANA_CPUQ_CFG_CPUQ_MLD_SET(LAN9645X_CPUQ_MLD) |
+ ANA_CPUQ_CFG_CPUQ_IPMC_CTRL_SET(LAN9645X_CPUQ_IPMC_CTRL),
+ ANA_CPUQ_CFG_CPUQ_IGMP |
+ ANA_CPUQ_CFG_CPUQ_MLD |
+ ANA_CPUQ_CFG_CPUQ_IPMC_CTRL,
+ lan9645x, ANA_CPUQ_CFG);
+}
+
+static enum macaccess_entry_type lan9645x_mdb_classify(const unsigned char *mac)
+{
+ if (ether_addr_is_ipv4_mcast(mac))
+ return ENTRYTYPE_MACV4;
+ if (ether_addr_is_ipv6_mcast(mac))
+ return ENTRYTYPE_MACV6;
+ return ENTRYTYPE_LOCKED;
+}
+
+static struct lan9645x_mdb_entry *
+lan9645x_mdb_entry_lookup(struct lan9645x *lan9645x, const unsigned char *mac,
+ u16 vid)
+{
+ struct lan9645x_mdb_entry *mdb;
+
+ list_for_each_entry(mdb, &lan9645x->mdb_entries, list) {
+ if (ether_addr_equal(mdb->mac, mac) && mdb->vid == vid)
+ return mdb;
+ }
+
+ return NULL;
+}
+
+static struct lan9645x_mdb_entry *
+lan9645x_mdb_entry_alloc(struct lan9645x *lan9645x,
+ const unsigned char addr[ETH_ALEN], u16 vid)
+{
+ struct lan9645x_mdb_entry *mdb_entry;
+
+ mdb_entry = kzalloc_obj(*mdb_entry);
+ if (!mdb_entry)
+ return ERR_PTR(-ENOMEM);
+
+ ether_addr_copy(mdb_entry->mac, addr);
+ mdb_entry->vid = vid;
+
+ list_add_tail(&mdb_entry->list, &lan9645x->mdb_entries);
+
+ dev_dbg(lan9645x->dev, "vid=%u addr=%pM\n", mdb_entry->vid,
+ mdb_entry->mac);
+
+ return mdb_entry;
+}
+
+static void lan9645x_mdb_encode_mac(unsigned char *dst, unsigned char *mac,
+ u16 ports, enum macaccess_entry_type type)
+{
+ ether_addr_copy(dst, mac);
+
+ /* The HW encodes the portmask in the high bits of the mac for ip
+ * multicast entries, to save on the limited PGID resources.
+ *
+ * IPv4 Multicast DMAC: 0x01005Exxxxxx
+ * IPv6 Multicast DMAC: 0x3333xxxxxxxx
+ *
+ * which gives us 24 or 16 bits to encode the portmask.
+ */
+ if (type == ENTRYTYPE_MACV4) {
+ dst[0] = 0;
+ dst[1] = ports >> 8;
+ dst[2] = ports & 0xff;
+ } else if (type == ENTRYTYPE_MACV6) {
+ dst[0] = ports >> 8;
+ dst[1] = ports & 0xff;
+ }
+}
+
+static void lan9645x_pgid_entry_put(struct lan9645x *lan9645x,
+ struct lan9645x_pgid_entry *pgid_entry)
+{
+ if (!pgid_entry)
+ return;
+
+ if (!refcount_dec_and_test(&pgid_entry->refcount))
+ return;
+
+ dev_dbg(lan9645x->dev, "pgid=%d ports=0x%x", pgid_entry->index,
+ pgid_entry->ports);
+ /* We leave the PGID written in HW, as no entry is pointing to it. */
+ list_del(&pgid_entry->list);
+ kfree(pgid_entry);
+}
+
+static void lan9645x_mdb_entry_dealloc(struct lan9645x *lan9645x,
+ struct lan9645x_mdb_entry *mdb_entry)
+{
+ dev_dbg(lan9645x->dev, "vid=%u addr=%pM\n", mdb_entry->vid,
+ mdb_entry->mac);
+ list_del(&mdb_entry->list);
+ lan9645x_pgid_entry_put(lan9645x, mdb_entry->pgid);
+ kfree(mdb_entry);
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_mdb_pgid_entry_lookup(struct lan9645x *lan9645x, u16 ports)
+{
+ struct lan9645x_pgid_entry *pgid_entry;
+
+ list_for_each_entry(pgid_entry, &lan9645x->pgid_entries, list) {
+ if (pgid_entry->ports == ports &&
+ refcount_inc_not_zero(&pgid_entry->refcount))
+ return pgid_entry;
+ }
+
+ return NULL;
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_pgid_entry_alloc(struct lan9645x *lan9645x, int index, u16 ports)
+{
+ struct lan9645x_pgid_entry *pgid_entry;
+
+ pgid_entry = kzalloc_obj(*pgid_entry);
+ if (!pgid_entry)
+ return ERR_PTR(-ENOMEM);
+
+ pgid_entry->ports = ports;
+ pgid_entry->index = index;
+ refcount_set(&pgid_entry->refcount, 1);
+
+ list_add_tail(&pgid_entry->list, &lan9645x->pgid_entries);
+
+ dev_dbg(lan9645x->dev, "index=%d ports=0x%x", pgid_entry->index,
+ pgid_entry->ports);
+
+ lan_rmw(ANA_PGID_PGID_SET(pgid_entry->ports),
+ ANA_PGID_PGID, lan9645x,
+ ANA_PGID(pgid_entry->index));
+
+ return pgid_entry;
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_mdb_pgid_entry_create(struct lan9645x *lan9645x, u16 ports)
+{
+ struct lan9645x_pgid_entry *pgid_entry = NULL;
+ int index;
+
+ for (index = PGID_GP_START; index < PGID_GP_END; index++) {
+ bool used = false;
+
+ list_for_each_entry(pgid_entry, &lan9645x->pgid_entries, list) {
+ if (pgid_entry->index == index) {
+ used = true;
+ break;
+ }
+ }
+
+ if (!used)
+ return lan9645x_pgid_entry_alloc(lan9645x, index,
+ ports);
+ }
+
+ return ERR_PTR(-ENOSPC);
+}
+
+static struct lan9645x_pgid_entry *
+lan9645x_mdb_pgid_entry_get(struct lan9645x *lan9645x, u16 ports,
+ enum macaccess_entry_type type)
+{
+ struct lan9645x_pgid_entry *pgid_entry;
+ u16 pgid_ports;
+
+ if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6 || !ports)
+ return NULL;
+
+ /* CPU_PORT forwarding is handled by cpu_copy flag on mac table entry.
+ * So we can strip CPU_PORT here to allow better PGID sharing.
+ */
+ pgid_ports = ports & ~BIT(CPU_PORT);
+
+ pgid_entry = lan9645x_mdb_pgid_entry_lookup(lan9645x, pgid_ports);
+ if (!pgid_entry)
+ return lan9645x_mdb_pgid_entry_create(lan9645x, pgid_ports);
+
+ return pgid_entry;
+}
+
+static int lan9645x_mdb_update_dest(struct lan9645x *lan9645x,
+ struct lan9645x_mdb_entry *mdb_entry,
+ enum macaccess_entry_type type,
+ struct lan9645x_pgid_entry *new_pgid,
+ u16 new_ports)
+{
+ struct lan9645x_pgid_entry *old_pgid;
+ unsigned char mac[ETH_ALEN];
+ int err, pgid_index;
+ bool cpu_copy;
+
+ old_pgid = mdb_entry->pgid;
+ lan9645x_mdb_encode_mac(mac, mdb_entry->mac, new_ports, type);
+ cpu_copy = !!(new_ports & BIT(CPU_PORT));
+ pgid_index = PGID_INDEX(new_pgid);
+
+ err = lan9645x_mact_learn_cpu_copy(lan9645x, pgid_index, mac,
+ mdb_entry->vid, type, cpu_copy);
+ if (err) {
+ lan9645x_pgid_entry_put(lan9645x, new_pgid);
+ return err;
+ }
+ mdb_entry->pgid = new_pgid;
+ mdb_entry->ports = new_ports;
+ lan9645x_pgid_entry_put(lan9645x, old_pgid);
+ return 0;
+}
+
+static int __lan9645x_mdb_add(struct lan9645x *lan9645x, int chip_port,
+ const unsigned char addr[ETH_ALEN], u16 vid,
+ enum macaccess_entry_type type)
+{
+ struct lan9645x_pgid_entry *new_pgid;
+ struct lan9645x_mdb_entry *mdb_entry;
+ u16 new_ports;
+
+ mdb_entry = lan9645x_mdb_entry_lookup(lan9645x, addr, vid);
+ if (!mdb_entry) {
+ mdb_entry = lan9645x_mdb_entry_alloc(lan9645x, addr, vid);
+ if (IS_ERR(mdb_entry))
+ return PTR_ERR(mdb_entry);
+ }
+
+ if (mdb_entry->ports & BIT(chip_port))
+ return 0;
+
+ new_ports = mdb_entry->ports | BIT(chip_port);
+
+ /* Update PGID ptr for non-IP entries (L2 multicast) */
+ new_pgid = lan9645x_mdb_pgid_entry_get(lan9645x, new_ports, type);
+ if (IS_ERR(new_pgid)) {
+ /* Out of PGIDs or mem. Continue forwarding to old port
+ * group, or remove if fresh mdb_entry.
+ */
+ if (!mdb_entry->ports)
+ lan9645x_mdb_entry_dealloc(lan9645x, mdb_entry);
+
+ return PTR_ERR(new_pgid);
+ }
+
+ return lan9645x_mdb_update_dest(lan9645x, mdb_entry, type, new_pgid,
+ new_ports);
+}
+
+static int __lan9645x_mdb_del(struct lan9645x *lan9645x, int chip_port,
+ const unsigned char addr[ETH_ALEN], u16 vid,
+ enum macaccess_entry_type type)
+{
+ struct lan9645x_pgid_entry *new_pgid;
+ struct lan9645x_mdb_entry *mdb_entry;
+ u16 new_ports;
+ int err;
+
+ mdb_entry = lan9645x_mdb_entry_lookup(lan9645x, addr, vid);
+ if (!mdb_entry)
+ return -ENOENT;
+
+ if (!(mdb_entry->ports & BIT(chip_port)))
+ return 0;
+
+ new_ports = mdb_entry->ports & ~BIT(chip_port);
+
+ if (!new_ports) {
+ err = lan9645x_mact_forget(lan9645x, mdb_entry->mac,
+ mdb_entry->vid, type);
+ if (err)
+ return err;
+ lan9645x_mdb_entry_dealloc(lan9645x, mdb_entry);
+ return 0;
+ }
+
+ /* Update PGID ptr for non-IP entries (L2 multicast) */
+ new_pgid = lan9645x_mdb_pgid_entry_get(lan9645x, new_ports, type);
+ if (IS_ERR(new_pgid))
+ /* Continue forwarding to old port group. */
+ return PTR_ERR(new_pgid);
+
+ return lan9645x_mdb_update_dest(lan9645x, mdb_entry, type, new_pgid,
+ new_ports);
+}
+
+static int lan9645x_mdb_add(struct lan9645x *lan9645x, int chip_port,
+ const unsigned char addr[ETH_ALEN], u16 vid,
+ enum macaccess_entry_type type)
+{
+ int err;
+
+ mutex_lock(&lan9645x->mdb_lock);
+ err = __lan9645x_mdb_add(lan9645x, chip_port, addr, vid, type);
+ mutex_unlock(&lan9645x->mdb_lock);
+ return err;
+}
+
+static int lan9645x_mdb_del(struct lan9645x *lan9645x, int chip_port,
+ const unsigned char addr[ETH_ALEN], u16 vid,
+ enum macaccess_entry_type type)
+{
+ int err;
+
+ mutex_lock(&lan9645x->mdb_lock);
+ err = __lan9645x_mdb_del(lan9645x, chip_port, addr, vid, type);
+ mutex_unlock(&lan9645x->mdb_lock);
+ return err;
+}
+
+int lan9645x_mdb_port_add(struct lan9645x *lan9645x, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ struct net_device *bridge)
+{
+ enum macaccess_entry_type type;
+ u16 vid = mdb->vid;
+
+ type = lan9645x_mdb_classify(mdb->addr);
+
+ if (!vid)
+ vid = lan9645x_vlan_unaware_pvid(!!bridge);
+
+ return lan9645x_mdb_add(lan9645x, port, mdb->addr, vid, type);
+}
+
+int lan9645x_mdb_port_del(struct lan9645x *lan9645x, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ struct net_device *bridge)
+{
+ enum macaccess_entry_type type;
+ u16 vid = mdb->vid;
+
+ type = lan9645x_mdb_classify(mdb->addr);
+
+ if (!vid)
+ vid = lan9645x_vlan_unaware_pvid(!!bridge);
+
+ return lan9645x_mdb_del(lan9645x, port, mdb->addr, vid, type);
+}
+
+void lan9645x_mdb_deinit(struct lan9645x *lan9645x)
+{
+ struct lan9645x_mdb_entry *mdb, *tmp;
+
+ list_for_each_entry_safe(mdb, tmp, &lan9645x->mdb_entries, list)
+ lan9645x_mdb_entry_dealloc(lan9645x, mdb);
+
+ mutex_destroy(&lan9645x->mdb_lock);
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
index 661cd00465e2..5dee92940f89 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_port.c
@@ -189,6 +189,14 @@ int lan9645x_port_setup(struct dsa_switch *ds, int port)
ANA_PORT_CFG_PORTID_VAL,
lan9645x, ANA_PORT_CFG(p->chip_port));
+ lan_rmw(ANA_CPU_FWD_CFG_IGMP_REDIR_ENA_SET(true) |
+ ANA_CPU_FWD_CFG_MLD_REDIR_ENA_SET(true) |
+ ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA_SET(true),
+ ANA_CPU_FWD_CFG_IGMP_REDIR_ENA |
+ ANA_CPU_FWD_CFG_MLD_REDIR_ENA |
+ ANA_CPU_FWD_CFG_IPMC_CTRL_COPY_ENA,
+ lan9645x, ANA_CPU_FWD_CFG(p->chip_port));
+
if (p->chip_port != lan9645x->npi)
lan9645x_vlan_set_hostmode(p);
diff --git a/include/linux/dsa/lan9645x.h b/include/linux/dsa/lan9645x.h
index 34c18bf975d0..ac0e70c704a5 100644
--- a/include/linux/dsa/lan9645x.h
+++ b/include/linux/dsa/lan9645x.h
@@ -131,4 +131,15 @@
#define IFH_DUPL_DISC_ENA_SZ 1
#define IFH_RCT_AVAIL_SZ 1
+/* Chip has 8 cpu queues. The cpu queues used by a frame is passed as a mask in
+ * the IFH on extraction. We use this to avoid classifying IGMP and MLD frames
+ * in the tag driver.
+ */
+enum {
+ LAN9645X_CPUQ_DEF = 0,
+ LAN9645X_CPUQ_IGMP = 1,
+ LAN9645X_CPUQ_MLD = 2,
+ LAN9645X_CPUQ_IPMC_CTRL = 3,
+};
+
#endif /* _NET_DSA_TAG_LAN9645X_H_ */
diff --git a/net/dsa/tag_lan9645x.c b/net/dsa/tag_lan9645x.c
index 6dae3b9ec240..86d67426bf04 100644
--- a/net/dsa/tag_lan9645x.c
+++ b/net/dsa/tag_lan9645x.c
@@ -153,6 +153,20 @@ static void lan9645x_xmit_get_vlan_info(struct sk_buff *skb,
LAN9645X_IFH_TAG_TYPE_C;
}
+static void lan9645x_offload_fwd_mark(struct sk_buff *skb, u32 cpuq)
+{
+ u32 cpu_redir;
+
+ /* IGMP/MLD are trapped to CPU, and must be forwarded by the stack */
+ cpu_redir = BIT(LAN9645X_CPUQ_IGMP) | BIT(LAN9645X_CPUQ_MLD);
+ if (cpuq & cpu_redir) {
+ skb->offload_fwd_mark = 0;
+ return;
+ }
+
+ return dsa_default_offload_fwd_mark(skb);
+}
+
static struct sk_buff *lan9645x_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
@@ -193,7 +207,7 @@ static struct sk_buff *lan9645x_xmit(struct sk_buff *skb,
static struct sk_buff *lan9645x_rcv(struct sk_buff *skb,
struct net_device *ndev)
{
- u32 src_port, qos_class, vlan_tci, tag_type, popcnt, etype_ofs;
+ u32 src_port, qos_class, vlan_tci, tag_type, popcnt, etype_ofs, cpuq;
struct dsa_port *dp;
u32 ifh_gap_len = 0;
u16 vlan_tpid;
@@ -216,6 +230,7 @@ static struct sk_buff *lan9645x_rcv(struct sk_buff *skb,
tag_type = lan9645x_ifh_get(ifh, IFH_TAG_TYPE, IFH_TAG_TYPE_SZ);
vlan_tci = lan9645x_ifh_get(ifh, IFH_TCI, IFH_TCI_SZ);
qos_class = lan9645x_ifh_get(ifh, IFH_QOS_CLASS, IFH_QOS_CLASS_SZ);
+ cpuq = lan9645x_ifh_get(ifh, IFH_CPUQ, IFH_CPUQ_SZ);
/* Set skb->data at start of real header
*
@@ -252,7 +267,7 @@ static struct sk_buff *lan9645x_rcv(struct sk_buff *skb,
return NULL;
}
- dsa_default_offload_fwd_mark(skb);
+ lan9645x_offload_fwd_mark(skb, cpuq);
skb->priority = qos_class;
--
2.52.0
^ permalink raw reply related
* [PATCH net-next v3 9/9] net: dsa: lan9645x: add port statistics
From: Jens Emil Schulz Østergaard @ 2026-04-10 11:48 UTC (permalink / raw)
To: UNGLinuxDriver, Andrew Lunn, Vladimir Oltean, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Woojung Huh,
Russell King, Steen Hegelund, Daniel Machon
Cc: linux-kernel, netdev, devicetree,
Jens Emil Schulz Østergaard
In-Reply-To: <20260410-dsa_lan9645x_switch_driver_base-v3-0-aadc8595306d@microchip.com>
Add statistics support for the port counters. Chip registers are 32 bit,
so this unit is responsible maintaining a 64bit software cache, and
updating it frequently to handle overflows in hardware.
Reviewed-by: Steen Hegelund <Steen.Hegelund@microchip.com>
Signed-off-by: Jens Emil Schulz Østergaard <jensemil.schulzostergaard@microchip.com>
---
Changes in v3:
- No changes
Changes in v2:
- introduce spinlock sw_lock protecting software counters and region
buffer.
- ran Ioana's selftest for standard counters
drivers/net/hw/ethtool_std_stats.sh
along with
selftests/drivers/net/hw/ethtool_rmon.sh
they pass except for software injected pause frames.
- remove strings/counters covered by standard counters from
get_strings/get_ethtool_stats.
- fix proper use of 'src' in standard counters
- remove static region table, and use stats_prepare_regions for dynamic
region calculation inspired by ocelot.
- fix queue leak in error path.
---
drivers/net/dsa/microchip/lan9645x/Makefile | 1 +
drivers/net/dsa/microchip/lan9645x/lan9645x_main.c | 86 ++
drivers/net/dsa/microchip/lan9645x/lan9645x_main.h | 3 +
.../net/dsa/microchip/lan9645x/lan9645x_stats.c | 922 +++++++++++++++++++++
.../net/dsa/microchip/lan9645x/lan9645x_stats.h | 277 +++++++
5 files changed, 1289 insertions(+)
diff --git a/drivers/net/dsa/microchip/lan9645x/Makefile b/drivers/net/dsa/microchip/lan9645x/Makefile
index 2413d11fe849..cd994943c1c0 100644
--- a/drivers/net/dsa/microchip/lan9645x/Makefile
+++ b/drivers/net/dsa/microchip/lan9645x/Makefile
@@ -8,4 +8,5 @@ mchp-lan9645x-objs := \
lan9645x_npi.o \
lan9645x_phylink.o \
lan9645x_port.o \
+ lan9645x_stats.o \
lan9645x_vlan.o \
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
index 8f63729ff55d..738a01ee6e64 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.c
@@ -5,6 +5,7 @@
#include <linux/platform_device.h>
#include "lan9645x_main.h"
+#include "lan9645x_stats.h"
static const char *lan9645x_resource_names[NUM_TARGETS + 1] = {
[TARGET_GCB] = "gcb",
@@ -73,6 +74,7 @@ static void lan9645x_teardown(struct dsa_switch *ds)
lan9645x_npi_port_deinit(lan9645x, lan9645x->npi);
lan9645x_mac_deinit(lan9645x);
lan9645x_mdb_deinit(lan9645x);
+ lan9645x_stats_deinit(lan9645x);
mutex_destroy(&lan9645x->fwd_domain_lock);
}
@@ -266,6 +268,12 @@ static int lan9645x_setup(struct dsa_switch *ds)
if (!lan9645x->owq)
return -ENOMEM;
+ err = lan9645x_stats_init(lan9645x);
+ if (err) {
+ dev_err(dev, "Failed to init stats.\n");
+ goto owq_destroy;
+ }
+
ds->mtu_enforcement_ingress = true;
ds->assisted_learning_on_cpu_port = true;
ds->fdb_isolation = true;
@@ -276,6 +284,10 @@ static int lan9645x_setup(struct dsa_switch *ds)
lan9645x->num_phys_ports - lan9645x->num_port_dis);
return 0;
+
+owq_destroy:
+ destroy_workqueue(lan9645x->owq);
+ return err;
}
static void lan9645x_port_phylink_get_caps(struct dsa_switch *ds, int port,
@@ -737,6 +749,68 @@ static int lan9645x_mdb_del(struct dsa_switch *ds, int port,
return err;
}
+static void lan9645x_get_strings(struct dsa_switch *ds, int port, u32 stringset,
+ uint8_t *data)
+{
+ lan9645x_stats_get_strings(ds->priv, port, stringset, data);
+}
+
+static void lan9645x_get_ethtool_stats(struct dsa_switch *ds, int port,
+ uint64_t *data)
+{
+ lan9645x_stats_get_ethtool_stats(ds->priv, port, data);
+}
+
+static int lan9645x_get_sset_count(struct dsa_switch *ds, int port, int sset)
+{
+ return lan9645x_stats_get_sset_count(ds->priv, port, sset);
+}
+
+static void lan9645x_get_eth_mac_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_mac_stats *mac_stats)
+{
+ lan9645x_stats_get_eth_mac_stats(ds->priv, port, mac_stats);
+}
+
+static void
+lan9645x_get_rmon_stats(struct dsa_switch *ds, int port,
+ struct ethtool_rmon_stats *rmon_stats,
+ const struct ethtool_rmon_hist_range **ranges)
+{
+ lan9645x_stats_get_rmon_stats(ds->priv, port, rmon_stats, ranges);
+}
+
+static void lan9645x_get_stats64(struct dsa_switch *ds, int port,
+ struct rtnl_link_stats64 *s)
+{
+ lan9645x_stats_get_stats64(ds->priv, port, s);
+}
+
+static void lan9645x_get_pause_stats(struct dsa_switch *ds, int port,
+ struct ethtool_pause_stats *pause_stats)
+{
+ lan9645x_stats_get_pause_stats(ds->priv, port, pause_stats);
+}
+
+static void lan9645x_get_mm_stats(struct dsa_switch *ds, int port,
+ struct ethtool_mm_stats *stats)
+{
+ lan9645x_stats_get_mm_stats(ds->priv, port, stats);
+}
+
+static void lan9645x_get_eth_phy_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_phy_stats *phy_stats)
+{
+ lan9645x_stats_get_eth_phy_stats(ds->priv, port, phy_stats);
+}
+
+static void
+lan9645x_get_eth_ctrl_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_ctrl_stats *ctrl_stats)
+{
+ lan9645x_stats_get_eth_ctrl_stats(ds->priv, port, ctrl_stats);
+}
+
static const struct dsa_switch_ops lan9645x_switch_ops = {
.get_tag_protocol = lan9645x_get_tag_protocol,
@@ -774,6 +848,18 @@ static const struct dsa_switch_ops lan9645x_switch_ops = {
/* Multicast database */
.port_mdb_add = lan9645x_mdb_add,
.port_mdb_del = lan9645x_mdb_del,
+
+ /* Port statistics counters. */
+ .get_strings = lan9645x_get_strings,
+ .get_ethtool_stats = lan9645x_get_ethtool_stats,
+ .get_sset_count = lan9645x_get_sset_count,
+ .get_eth_mac_stats = lan9645x_get_eth_mac_stats,
+ .get_rmon_stats = lan9645x_get_rmon_stats,
+ .get_stats64 = lan9645x_get_stats64,
+ .get_pause_stats = lan9645x_get_pause_stats,
+ .get_mm_stats = lan9645x_get_mm_stats,
+ .get_eth_phy_stats = lan9645x_get_eth_phy_stats,
+ .get_eth_ctrl_stats = lan9645x_get_eth_ctrl_stats,
};
static int lan9645x_request_target_regmaps(struct lan9645x *lan9645x)
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
index 1c3c9899ed07..8d40eb1b6103 100644
--- a/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_main.h
@@ -226,6 +226,9 @@ struct lan9645x {
*/
struct mutex mdb_lock;
+ /* Statistics */
+ struct lan9645x_stats *stats;
+
int num_port_dis;
bool dd_dis;
bool tsn_dis;
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.c b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.c
new file mode 100644
index 000000000000..3e82a859f173
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.c
@@ -0,0 +1,922 @@
+// SPDX-License-Identifier: GPL-2.0+
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+
+#include "lan9645x_main.h"
+#include "lan9645x_stats.h"
+
+#define LAN9645X_STATS_CHECK_DELAY (3 * HZ)
+
+static const u32 lan9645x_port_stats_layout[] = {
+ [SCNT_RX_OCT] = 0x0,
+ [SCNT_RX_UC] = 0x1,
+ [SCNT_RX_MC] = 0x2,
+ [SCNT_RX_BC] = 0x3,
+ [SCNT_RX_SHORT] = 0x4,
+ [SCNT_RX_FRAG] = 0x5,
+ [SCNT_RX_JABBER] = 0x6,
+ [SCNT_RX_CRC] = 0x7,
+ [SCNT_RX_SYMBOL_ERR] = 0x8,
+ [SCNT_RX_SZ_64] = 0x9,
+ [SCNT_RX_SZ_65_127] = 0xa,
+ [SCNT_RX_SZ_128_255] = 0xb,
+ [SCNT_RX_SZ_256_511] = 0xc,
+ [SCNT_RX_SZ_512_1023] = 0xd,
+ [SCNT_RX_SZ_1024_1526] = 0xe,
+ [SCNT_RX_SZ_JUMBO] = 0xf,
+ [SCNT_RX_PAUSE] = 0x10,
+ [SCNT_RX_CONTROL] = 0x11,
+ [SCNT_RX_LONG] = 0x12,
+ [SCNT_RX_CAT_DROP] = 0x13,
+ [SCNT_RX_RED_PRIO_0] = 0x14,
+ [SCNT_RX_RED_PRIO_1] = 0x15,
+ [SCNT_RX_RED_PRIO_2] = 0x16,
+ [SCNT_RX_RED_PRIO_3] = 0x17,
+ [SCNT_RX_RED_PRIO_4] = 0x18,
+ [SCNT_RX_RED_PRIO_5] = 0x19,
+ [SCNT_RX_RED_PRIO_6] = 0x1a,
+ [SCNT_RX_RED_PRIO_7] = 0x1b,
+ [SCNT_RX_YELLOW_PRIO_0] = 0x1c,
+ [SCNT_RX_YELLOW_PRIO_1] = 0x1d,
+ [SCNT_RX_YELLOW_PRIO_2] = 0x1e,
+ [SCNT_RX_YELLOW_PRIO_3] = 0x1f,
+ [SCNT_RX_YELLOW_PRIO_4] = 0x20,
+ [SCNT_RX_YELLOW_PRIO_5] = 0x21,
+ [SCNT_RX_YELLOW_PRIO_6] = 0x22,
+ [SCNT_RX_YELLOW_PRIO_7] = 0x23,
+ [SCNT_RX_GREEN_PRIO_0] = 0x24,
+ [SCNT_RX_GREEN_PRIO_1] = 0x25,
+ [SCNT_RX_GREEN_PRIO_2] = 0x26,
+ [SCNT_RX_GREEN_PRIO_3] = 0x27,
+ [SCNT_RX_GREEN_PRIO_4] = 0x28,
+ [SCNT_RX_GREEN_PRIO_5] = 0x29,
+ [SCNT_RX_GREEN_PRIO_6] = 0x2a,
+ [SCNT_RX_GREEN_PRIO_7] = 0x2b,
+ [SCNT_RX_ASSEMBLY_ERR] = 0x2c,
+ [SCNT_RX_SMD_ERR] = 0x2d,
+ [SCNT_RX_ASSEMBLY_OK] = 0x2e,
+ [SCNT_RX_MERGE_FRAG] = 0x2f,
+ [SCNT_RX_PMAC_OCT] = 0x30,
+ [SCNT_RX_PMAC_UC] = 0x31,
+ [SCNT_RX_PMAC_MC] = 0x32,
+ [SCNT_RX_PMAC_BC] = 0x33,
+ [SCNT_RX_PMAC_SHORT] = 0x34,
+ [SCNT_RX_PMAC_FRAG] = 0x35,
+ [SCNT_RX_PMAC_JABBER] = 0x36,
+ [SCNT_RX_PMAC_CRC] = 0x37,
+ [SCNT_RX_PMAC_SYMBOL_ERR] = 0x38,
+ [SCNT_RX_PMAC_SZ_64] = 0x39,
+ [SCNT_RX_PMAC_SZ_65_127] = 0x3a,
+ [SCNT_RX_PMAC_SZ_128_255] = 0x3b,
+ [SCNT_RX_PMAC_SZ_256_511] = 0x3c,
+ [SCNT_RX_PMAC_SZ_512_1023] = 0x3d,
+ [SCNT_RX_PMAC_SZ_1024_1526] = 0x3e,
+ [SCNT_RX_PMAC_SZ_JUMBO] = 0x3f,
+ [SCNT_RX_PMAC_PAUSE] = 0x40,
+ [SCNT_RX_PMAC_CONTROL] = 0x41,
+ [SCNT_RX_PMAC_LONG] = 0x42,
+ [SCNT_TX_OCT] = 0x80,
+ [SCNT_TX_UC] = 0x81,
+ [SCNT_TX_MC] = 0x82,
+ [SCNT_TX_BC] = 0x83,
+ [SCNT_TX_COL] = 0x84,
+ [SCNT_TX_DROP] = 0x85,
+ [SCNT_TX_PAUSE] = 0x86,
+ [SCNT_TX_SZ_64] = 0x87,
+ [SCNT_TX_SZ_65_127] = 0x88,
+ [SCNT_TX_SZ_128_255] = 0x89,
+ [SCNT_TX_SZ_256_511] = 0x8a,
+ [SCNT_TX_SZ_512_1023] = 0x8b,
+ [SCNT_TX_SZ_1024_1526] = 0x8c,
+ [SCNT_TX_SZ_JUMBO] = 0x8d,
+ [SCNT_TX_YELLOW_PRIO_0] = 0x8e,
+ [SCNT_TX_YELLOW_PRIO_1] = 0x8f,
+ [SCNT_TX_YELLOW_PRIO_2] = 0x90,
+ [SCNT_TX_YELLOW_PRIO_3] = 0x91,
+ [SCNT_TX_YELLOW_PRIO_4] = 0x92,
+ [SCNT_TX_YELLOW_PRIO_5] = 0x93,
+ [SCNT_TX_YELLOW_PRIO_6] = 0x94,
+ [SCNT_TX_YELLOW_PRIO_7] = 0x95,
+ [SCNT_TX_GREEN_PRIO_0] = 0x96,
+ [SCNT_TX_GREEN_PRIO_1] = 0x97,
+ [SCNT_TX_GREEN_PRIO_2] = 0x98,
+ [SCNT_TX_GREEN_PRIO_3] = 0x99,
+ [SCNT_TX_GREEN_PRIO_4] = 0x9a,
+ [SCNT_TX_GREEN_PRIO_5] = 0x9b,
+ [SCNT_TX_GREEN_PRIO_6] = 0x9c,
+ [SCNT_TX_GREEN_PRIO_7] = 0x9d,
+ [SCNT_TX_AGED] = 0x9e,
+ [SCNT_TX_LLCT] = 0x9f,
+ [SCNT_TX_CT] = 0xa0,
+ [SCNT_TX_BUFDROP] = 0xa1,
+ [SCNT_TX_MM_HOLD] = 0xa2,
+ [SCNT_TX_MERGE_FRAG] = 0xa3,
+ [SCNT_TX_PMAC_OCT] = 0xa4,
+ [SCNT_TX_PMAC_UC] = 0xa5,
+ [SCNT_TX_PMAC_MC] = 0xa6,
+ [SCNT_TX_PMAC_BC] = 0xa7,
+ [SCNT_TX_PMAC_PAUSE] = 0xa8,
+ [SCNT_TX_PMAC_SZ_64] = 0xa9,
+ [SCNT_TX_PMAC_SZ_65_127] = 0xaa,
+ [SCNT_TX_PMAC_SZ_128_255] = 0xab,
+ [SCNT_TX_PMAC_SZ_256_511] = 0xac,
+ [SCNT_TX_PMAC_SZ_512_1023] = 0xad,
+ [SCNT_TX_PMAC_SZ_1024_1526] = 0xae,
+ [SCNT_TX_PMAC_SZ_JUMBO] = 0xaf,
+ [SCNT_DR_LOCAL] = 0x100,
+ [SCNT_DR_TAIL] = 0x101,
+ [SCNT_DR_YELLOW_PRIO_0] = 0x102,
+ [SCNT_DR_YELLOW_PRIO_1] = 0x103,
+ [SCNT_DR_YELLOW_PRIO_2] = 0x104,
+ [SCNT_DR_YELLOW_PRIO_3] = 0x105,
+ [SCNT_DR_YELLOW_PRIO_4] = 0x106,
+ [SCNT_DR_YELLOW_PRIO_5] = 0x107,
+ [SCNT_DR_YELLOW_PRIO_6] = 0x108,
+ [SCNT_DR_YELLOW_PRIO_7] = 0x109,
+ [SCNT_DR_GREEN_PRIO_0] = 0x10a,
+ [SCNT_DR_GREEN_PRIO_1] = 0x10b,
+ [SCNT_DR_GREEN_PRIO_2] = 0x10c,
+ [SCNT_DR_GREEN_PRIO_3] = 0x10d,
+ [SCNT_DR_GREEN_PRIO_4] = 0x10e,
+ [SCNT_DR_GREEN_PRIO_5] = 0x10f,
+ [SCNT_DR_GREEN_PRIO_6] = 0x110,
+ [SCNT_DR_GREEN_PRIO_7] = 0x111,
+};
+
+struct lan9645x_ethtool_stat {
+ char name[ETH_GSTRING_LEN];
+ u16 idx;
+};
+
+static const struct lan9645x_ethtool_stat lan9645x_port_ethtool_stats[] = {
+ { "rx_uc", SCNT_RX_UC },
+ { "rx_cat_drop", SCNT_RX_CAT_DROP },
+ { "rx_red_prio_0", SCNT_RX_RED_PRIO_0 },
+ { "rx_red_prio_1", SCNT_RX_RED_PRIO_1 },
+ { "rx_red_prio_2", SCNT_RX_RED_PRIO_2 },
+ { "rx_red_prio_3", SCNT_RX_RED_PRIO_3 },
+ { "rx_red_prio_4", SCNT_RX_RED_PRIO_4 },
+ { "rx_red_prio_5", SCNT_RX_RED_PRIO_5 },
+ { "rx_red_prio_6", SCNT_RX_RED_PRIO_6 },
+ { "rx_red_prio_7", SCNT_RX_RED_PRIO_7 },
+ { "rx_yellow_prio_0", SCNT_RX_YELLOW_PRIO_0 },
+ { "rx_yellow_prio_1", SCNT_RX_YELLOW_PRIO_1 },
+ { "rx_yellow_prio_2", SCNT_RX_YELLOW_PRIO_2 },
+ { "rx_yellow_prio_3", SCNT_RX_YELLOW_PRIO_3 },
+ { "rx_yellow_prio_4", SCNT_RX_YELLOW_PRIO_4 },
+ { "rx_yellow_prio_5", SCNT_RX_YELLOW_PRIO_5 },
+ { "rx_yellow_prio_6", SCNT_RX_YELLOW_PRIO_6 },
+ { "rx_yellow_prio_7", SCNT_RX_YELLOW_PRIO_7 },
+ { "rx_green_prio_0", SCNT_RX_GREEN_PRIO_0 },
+ { "rx_green_prio_1", SCNT_RX_GREEN_PRIO_1 },
+ { "rx_green_prio_2", SCNT_RX_GREEN_PRIO_2 },
+ { "rx_green_prio_3", SCNT_RX_GREEN_PRIO_3 },
+ { "rx_green_prio_4", SCNT_RX_GREEN_PRIO_4 },
+ { "rx_green_prio_5", SCNT_RX_GREEN_PRIO_5 },
+ { "rx_green_prio_6", SCNT_RX_GREEN_PRIO_6 },
+ { "rx_green_prio_7", SCNT_RX_GREEN_PRIO_7 },
+ { "tx_uc", SCNT_TX_UC },
+ { "tx_drop", SCNT_TX_DROP },
+ { "tx_yellow_prio_0", SCNT_TX_YELLOW_PRIO_0 },
+ { "tx_yellow_prio_1", SCNT_TX_YELLOW_PRIO_1 },
+ { "tx_yellow_prio_2", SCNT_TX_YELLOW_PRIO_2 },
+ { "tx_yellow_prio_3", SCNT_TX_YELLOW_PRIO_3 },
+ { "tx_yellow_prio_4", SCNT_TX_YELLOW_PRIO_4 },
+ { "tx_yellow_prio_5", SCNT_TX_YELLOW_PRIO_5 },
+ { "tx_yellow_prio_6", SCNT_TX_YELLOW_PRIO_6 },
+ { "tx_yellow_prio_7", SCNT_TX_YELLOW_PRIO_7 },
+ { "tx_green_prio_0", SCNT_TX_GREEN_PRIO_0 },
+ { "tx_green_prio_1", SCNT_TX_GREEN_PRIO_1 },
+ { "tx_green_prio_2", SCNT_TX_GREEN_PRIO_2 },
+ { "tx_green_prio_3", SCNT_TX_GREEN_PRIO_3 },
+ { "tx_green_prio_4", SCNT_TX_GREEN_PRIO_4 },
+ { "tx_green_prio_5", SCNT_TX_GREEN_PRIO_5 },
+ { "tx_green_prio_6", SCNT_TX_GREEN_PRIO_6 },
+ { "tx_green_prio_7", SCNT_TX_GREEN_PRIO_7 },
+ { "tx_aged", SCNT_TX_AGED },
+ { "tx_bufdrop", SCNT_TX_BUFDROP },
+ { "dr_local", SCNT_DR_LOCAL },
+ { "dr_tail", SCNT_DR_TAIL },
+ { "dr_yellow_prio_0", SCNT_DR_YELLOW_PRIO_0 },
+ { "dr_yellow_prio_1", SCNT_DR_YELLOW_PRIO_1 },
+ { "dr_yellow_prio_2", SCNT_DR_YELLOW_PRIO_2 },
+ { "dr_yellow_prio_3", SCNT_DR_YELLOW_PRIO_3 },
+ { "dr_yellow_prio_4", SCNT_DR_YELLOW_PRIO_4 },
+ { "dr_yellow_prio_5", SCNT_DR_YELLOW_PRIO_5 },
+ { "dr_yellow_prio_6", SCNT_DR_YELLOW_PRIO_6 },
+ { "dr_yellow_prio_7", SCNT_DR_YELLOW_PRIO_7 },
+ { "dr_green_prio_0", SCNT_DR_GREEN_PRIO_0 },
+ { "dr_green_prio_1", SCNT_DR_GREEN_PRIO_1 },
+ { "dr_green_prio_2", SCNT_DR_GREEN_PRIO_2 },
+ { "dr_green_prio_3", SCNT_DR_GREEN_PRIO_3 },
+ { "dr_green_prio_4", SCNT_DR_GREEN_PRIO_4 },
+ { "dr_green_prio_5", SCNT_DR_GREEN_PRIO_5 },
+ { "dr_green_prio_6", SCNT_DR_GREEN_PRIO_6 },
+ { "dr_green_prio_7", SCNT_DR_GREEN_PRIO_7 },
+};
+
+static const struct lan9645x_view_stats lan9645x_view_stat_cfgs[] = {
+ [LAN9645X_STAT_PORTS] = {
+ .name = "ports",
+ .type = LAN9645X_STAT_PORTS,
+ .layout = lan9645x_port_stats_layout,
+ .num_cnts = ARRAY_SIZE(lan9645x_port_stats_layout),
+ .num_indexes = NUM_PHYS_PORTS,
+ },
+};
+
+static int __lan9645x_stats_view_idx_hw_read(struct lan9645x *lan9645x,
+ enum lan9645x_view_stat_type vtype,
+ int idx)
+{
+ struct lan9645x_stat_region region;
+ struct lan9645x_view_stats *vstats;
+ u32 *region_buf;
+ int err;
+
+ lockdep_assert_held(&lan9645x->stats->hw_lock);
+
+ vstats = lan9645x_get_vstats(lan9645x, vtype);
+ if (!vstats || idx < 0 || idx >= vstats->num_indexes)
+ return -EINVAL;
+
+ lan_wr(SYS_STAT_CFG_STAT_VIEW_SET(idx), lan9645x, SYS_STAT_CFG);
+
+ region_buf = &vstats->buf[vstats->num_cnts * idx];
+
+ /* Each region for this index contains counters which are at sequential
+ * addresses, so we can use bulk reads to ease lock pressure a bit.
+ */
+ for (int r = 0; r < vstats->num_regions; r++) {
+ region = vstats->regions[r];
+ err = lan_bulk_rd(®ion_buf[region.cnts_base_idx], region.cnt,
+ lan9645x, SYS_CNT(region.base_offset));
+ if (err) {
+ dev_err(lan9645x->dev,
+ "stats bulk read err vtype=%d idx=%d err=%d\n",
+ vtype, idx, err);
+ return err;
+ }
+ }
+
+ return 0;
+}
+
+static void
+__lan9645x_stats_view_idx_transfer(struct lan9645x *lan9645x,
+ enum lan9645x_view_stat_type vtype, int idx)
+{
+ struct lan9645x_view_stats *vstats;
+ u64 *idx_counters;
+ u32 *region_buf;
+ int cntr;
+
+ lockdep_assert_held(&lan9645x->stats->sw_lock);
+
+ vstats = lan9645x_get_vstats(lan9645x, vtype);
+ if (!vstats || idx < 0 || idx >= vstats->num_indexes)
+ return;
+
+ idx_counters = STATS_INDEX(vstats, idx);
+ region_buf = &vstats->buf[vstats->num_cnts * idx];
+
+ for (cntr = 0; cntr < vstats->num_cnts; cntr++)
+ lan9645x_stats_add_cnt(&idx_counters[cntr], region_buf[cntr]);
+}
+
+static void __lan9645x_stats_view_idx_update(struct lan9645x *lan9645x,
+ enum lan9645x_view_stat_type vtype,
+ int idx)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+
+ lockdep_assert_held(&s->hw_lock);
+
+ if (!__lan9645x_stats_view_idx_hw_read(lan9645x, vtype, idx)) {
+ spin_lock(&s->sw_lock);
+ __lan9645x_stats_view_idx_transfer(lan9645x, vtype, idx);
+ spin_unlock(&s->sw_lock);
+ }
+}
+
+static u64 *lan9645x_stats_view_idx_update(struct lan9645x *lan9645x,
+ enum lan9645x_view_stat_type vtype,
+ int idx)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+
+ mutex_lock(&s->hw_lock);
+ __lan9645x_stats_view_idx_update(lan9645x, vtype, idx);
+ mutex_unlock(&s->hw_lock);
+
+ return STAT_COUNTERS(lan9645x, vtype, idx);
+}
+
+static void lan9645x_stats_view_update(struct lan9645x *lan9645x,
+ enum lan9645x_view_stat_type vtype)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ struct lan9645x_view_stats *vstats;
+ int idx;
+
+ vstats = lan9645x_get_vstats(lan9645x, vtype);
+ if (!vstats)
+ return;
+
+ switch (vtype) {
+ case LAN9645X_STAT_PORTS:
+ mutex_lock(&s->hw_lock);
+ for (idx = 0; idx < vstats->num_indexes; idx++) {
+ if (dsa_is_unused_port(lan9645x->ds, idx))
+ continue;
+ __lan9645x_stats_view_idx_update(lan9645x, vtype, idx);
+ }
+ mutex_unlock(&s->hw_lock);
+ return;
+ default:
+ return;
+ }
+}
+
+static void lan9645x_stats_update(struct lan9645x *lan9645x)
+{
+ for (int vtype = 0; vtype < LAN9645X_STAT_NUM; vtype++)
+ lan9645x_stats_view_update(lan9645x, vtype);
+}
+
+void lan9645x_stats_get_strings(struct lan9645x *lan9645x, int port,
+ u32 stringset, u8 *data)
+{
+ int i;
+
+ if (stringset != ETH_SS_STATS)
+ return;
+
+ for (i = 0; i < ARRAY_SIZE(lan9645x_port_ethtool_stats); i++)
+ memcpy(data + i * ETH_GSTRING_LEN,
+ lan9645x_port_ethtool_stats[i].name, ETH_GSTRING_LEN);
+}
+
+int lan9645x_stats_get_sset_count(struct lan9645x *lan9645x, int port, int sset)
+{
+ if (sset != ETH_SS_STATS)
+ return -EOPNOTSUPP;
+
+ return ARRAY_SIZE(lan9645x_port_ethtool_stats);
+}
+
+void lan9645x_stats_get_ethtool_stats(struct lan9645x *lan9645x, int port,
+ u64 *data)
+{
+ struct lan9645x_stats *stats = lan9645x->stats;
+ u64 *c;
+ int i;
+
+ c = lan9645x_stats_view_idx_update(lan9645x, LAN9645X_STAT_PORTS, port);
+
+ spin_lock(&stats->sw_lock);
+ for (i = 0; i < ARRAY_SIZE(lan9645x_port_ethtool_stats); i++)
+ *data++ = c[lan9645x_port_ethtool_stats[i].idx];
+ spin_unlock(&stats->sw_lock);
+}
+
+static u64 *lan9645x_stats_port_update(struct lan9645x *lan9645x, int port)
+{
+ return lan9645x_stats_view_idx_update(lan9645x, LAN9645X_STAT_PORTS,
+ port);
+}
+
+void lan9645x_stats_get_eth_mac_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_eth_mac_stats *m)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = lan9645x_stats_port_update(lan9645x, port);
+
+ spin_lock(&s->sw_lock);
+
+ switch (m->src) {
+ case ETHTOOL_MAC_STATS_SRC_EMAC:
+ m->FramesTransmittedOK = c[SCNT_TX_UC] +
+ c[SCNT_TX_MC] +
+ c[SCNT_TX_BC];
+ m->SingleCollisionFrames = c[SCNT_TX_COL];
+ m->FramesReceivedOK = c[SCNT_RX_UC] +
+ c[SCNT_RX_MC] +
+ c[SCNT_RX_BC];
+ m->FrameCheckSequenceErrors = c[SCNT_RX_CRC];
+ m->OctetsTransmittedOK = c[SCNT_TX_OCT];
+ m->OctetsReceivedOK = c[SCNT_RX_OCT];
+ m->MulticastFramesXmittedOK = c[SCNT_TX_MC];
+ m->BroadcastFramesXmittedOK = c[SCNT_TX_BC];
+ m->MulticastFramesReceivedOK = c[SCNT_RX_MC];
+ m->BroadcastFramesReceivedOK = c[SCNT_RX_BC];
+ m->InRangeLengthErrors = c[SCNT_RX_FRAG] +
+ c[SCNT_RX_JABBER] +
+ c[SCNT_RX_CRC];
+ m->OutOfRangeLengthField = c[SCNT_RX_SHORT] +
+ c[SCNT_RX_LONG];
+ m->FrameTooLongErrors = c[SCNT_RX_LONG];
+ break;
+ case ETHTOOL_MAC_STATS_SRC_PMAC:
+ m->FramesTransmittedOK = c[SCNT_TX_PMAC_UC] +
+ c[SCNT_TX_PMAC_MC] +
+ c[SCNT_TX_PMAC_BC];
+ m->FramesReceivedOK = c[SCNT_RX_PMAC_UC] +
+ c[SCNT_RX_PMAC_MC] +
+ c[SCNT_RX_PMAC_BC];
+ m->FrameCheckSequenceErrors = c[SCNT_RX_PMAC_CRC];
+ m->OctetsTransmittedOK = c[SCNT_TX_PMAC_OCT];
+ m->OctetsReceivedOK = c[SCNT_RX_PMAC_OCT];
+ m->MulticastFramesXmittedOK = c[SCNT_TX_PMAC_MC];
+ m->BroadcastFramesXmittedOK = c[SCNT_TX_PMAC_BC];
+ m->MulticastFramesReceivedOK = c[SCNT_RX_PMAC_MC];
+ m->BroadcastFramesReceivedOK = c[SCNT_RX_PMAC_BC];
+ m->InRangeLengthErrors = c[SCNT_RX_PMAC_FRAG] +
+ c[SCNT_RX_PMAC_JABBER] +
+ c[SCNT_RX_PMAC_CRC];
+ m->OutOfRangeLengthField = c[SCNT_RX_PMAC_SHORT] +
+ c[SCNT_RX_PMAC_LONG];
+ m->FrameTooLongErrors = c[SCNT_RX_PMAC_LONG];
+ break;
+ default:
+ m->FramesTransmittedOK = c[SCNT_TX_UC] +
+ c[SCNT_TX_MC] +
+ c[SCNT_TX_BC] +
+ c[SCNT_TX_PMAC_UC] +
+ c[SCNT_TX_PMAC_MC] +
+ c[SCNT_TX_PMAC_BC];
+ m->SingleCollisionFrames = c[SCNT_TX_COL];
+ m->FramesReceivedOK = c[SCNT_RX_UC] +
+ c[SCNT_RX_MC] +
+ c[SCNT_RX_BC] +
+ c[SCNT_RX_PMAC_UC] +
+ c[SCNT_RX_PMAC_MC] +
+ c[SCNT_RX_PMAC_BC];
+ m->FrameCheckSequenceErrors = c[SCNT_RX_CRC] +
+ c[SCNT_RX_PMAC_CRC];
+ m->OctetsTransmittedOK = c[SCNT_TX_OCT] +
+ c[SCNT_TX_PMAC_OCT];
+ m->OctetsReceivedOK = c[SCNT_RX_OCT] +
+ c[SCNT_RX_PMAC_OCT];
+ m->MulticastFramesXmittedOK = c[SCNT_TX_MC] +
+ c[SCNT_TX_PMAC_MC];
+ m->BroadcastFramesXmittedOK = c[SCNT_TX_BC] +
+ c[SCNT_TX_PMAC_BC];
+ m->MulticastFramesReceivedOK = c[SCNT_RX_MC] +
+ c[SCNT_RX_PMAC_MC];
+ m->BroadcastFramesReceivedOK = c[SCNT_RX_BC] +
+ c[SCNT_RX_PMAC_BC];
+ m->InRangeLengthErrors = c[SCNT_RX_FRAG] +
+ c[SCNT_RX_JABBER] +
+ c[SCNT_RX_CRC] +
+ c[SCNT_RX_PMAC_FRAG] +
+ c[SCNT_RX_PMAC_JABBER] +
+ c[SCNT_RX_PMAC_CRC];
+ m->OutOfRangeLengthField = c[SCNT_RX_SHORT] +
+ c[SCNT_RX_LONG] +
+ c[SCNT_RX_PMAC_SHORT] +
+ c[SCNT_RX_PMAC_LONG];
+ m->FrameTooLongErrors = c[SCNT_RX_LONG] +
+ c[SCNT_RX_PMAC_LONG];
+ break;
+ }
+
+ spin_unlock(&s->sw_lock);
+}
+
+static const struct ethtool_rmon_hist_range lan9645x_rmon_ranges[] = {
+ { 0, 64 },
+ { 65, 127 },
+ { 128, 255 },
+ { 256, 511 },
+ { 512, 1023 },
+ { 1024, 1526 },
+ { 1527, 0xffff },
+ {}
+};
+
+void
+lan9645x_stats_get_rmon_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_rmon_stats *r,
+ const struct ethtool_rmon_hist_range **ranges)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = lan9645x_stats_port_update(lan9645x, port);
+
+ spin_lock(&s->sw_lock);
+
+ switch (r->src) {
+ case ETHTOOL_MAC_STATS_SRC_EMAC:
+ r->undersize_pkts = c[SCNT_RX_SHORT];
+ r->oversize_pkts = c[SCNT_RX_LONG];
+ r->fragments = c[SCNT_RX_FRAG];
+ r->jabbers = c[SCNT_RX_JABBER];
+ r->hist[0] = c[SCNT_RX_SZ_64];
+ r->hist[1] = c[SCNT_RX_SZ_65_127];
+ r->hist[2] = c[SCNT_RX_SZ_128_255];
+ r->hist[3] = c[SCNT_RX_SZ_256_511];
+ r->hist[4] = c[SCNT_RX_SZ_512_1023];
+ r->hist[5] = c[SCNT_RX_SZ_1024_1526];
+ r->hist[6] = c[SCNT_RX_SZ_JUMBO];
+ r->hist_tx[0] = c[SCNT_TX_SZ_64];
+ r->hist_tx[1] = c[SCNT_TX_SZ_65_127];
+ r->hist_tx[2] = c[SCNT_TX_SZ_128_255];
+ r->hist_tx[3] = c[SCNT_TX_SZ_256_511];
+ r->hist_tx[4] = c[SCNT_TX_SZ_512_1023];
+ r->hist_tx[5] = c[SCNT_TX_SZ_1024_1526];
+ r->hist_tx[6] = c[SCNT_TX_SZ_JUMBO];
+ break;
+ case ETHTOOL_MAC_STATS_SRC_PMAC:
+ r->undersize_pkts = c[SCNT_RX_PMAC_SHORT];
+ r->oversize_pkts = c[SCNT_RX_PMAC_LONG];
+ r->fragments = c[SCNT_RX_PMAC_FRAG];
+ r->jabbers = c[SCNT_RX_PMAC_JABBER];
+ r->hist[0] = c[SCNT_RX_PMAC_SZ_64];
+ r->hist[1] = c[SCNT_RX_PMAC_SZ_65_127];
+ r->hist[2] = c[SCNT_RX_PMAC_SZ_128_255];
+ r->hist[3] = c[SCNT_RX_PMAC_SZ_256_511];
+ r->hist[4] = c[SCNT_RX_PMAC_SZ_512_1023];
+ r->hist[5] = c[SCNT_RX_PMAC_SZ_1024_1526];
+ r->hist[6] = c[SCNT_RX_PMAC_SZ_JUMBO];
+ r->hist_tx[0] = c[SCNT_TX_PMAC_SZ_64];
+ r->hist_tx[1] = c[SCNT_TX_PMAC_SZ_65_127];
+ r->hist_tx[2] = c[SCNT_TX_PMAC_SZ_128_255];
+ r->hist_tx[3] = c[SCNT_TX_PMAC_SZ_256_511];
+ r->hist_tx[4] = c[SCNT_TX_PMAC_SZ_512_1023];
+ r->hist_tx[5] = c[SCNT_TX_PMAC_SZ_1024_1526];
+ r->hist_tx[6] = c[SCNT_TX_PMAC_SZ_JUMBO];
+ break;
+ default:
+ r->undersize_pkts = c[SCNT_RX_SHORT] +
+ c[SCNT_RX_PMAC_SHORT];
+ r->oversize_pkts = c[SCNT_RX_LONG] +
+ c[SCNT_RX_PMAC_LONG];
+ r->fragments = c[SCNT_RX_FRAG] +
+ c[SCNT_RX_PMAC_FRAG];
+ r->jabbers = c[SCNT_RX_JABBER] +
+ c[SCNT_RX_PMAC_JABBER];
+ r->hist[0] = c[SCNT_RX_SZ_64] +
+ c[SCNT_RX_PMAC_SZ_64];
+ r->hist[1] = c[SCNT_RX_SZ_65_127] +
+ c[SCNT_RX_PMAC_SZ_65_127];
+ r->hist[2] = c[SCNT_RX_SZ_128_255] +
+ c[SCNT_RX_PMAC_SZ_128_255];
+ r->hist[3] = c[SCNT_RX_SZ_256_511] +
+ c[SCNT_RX_PMAC_SZ_256_511];
+ r->hist[4] = c[SCNT_RX_SZ_512_1023] +
+ c[SCNT_RX_PMAC_SZ_512_1023];
+ r->hist[5] = c[SCNT_RX_SZ_1024_1526] +
+ c[SCNT_RX_PMAC_SZ_1024_1526];
+ r->hist[6] = c[SCNT_RX_SZ_JUMBO] +
+ c[SCNT_RX_PMAC_SZ_JUMBO];
+ r->hist_tx[0] = c[SCNT_TX_SZ_64] +
+ c[SCNT_TX_PMAC_SZ_64];
+ r->hist_tx[1] = c[SCNT_TX_SZ_65_127] +
+ c[SCNT_TX_PMAC_SZ_65_127];
+ r->hist_tx[2] = c[SCNT_TX_SZ_128_255] +
+ c[SCNT_TX_PMAC_SZ_128_255];
+ r->hist_tx[3] = c[SCNT_TX_SZ_256_511] +
+ c[SCNT_TX_PMAC_SZ_256_511];
+ r->hist_tx[4] = c[SCNT_TX_SZ_512_1023] +
+ c[SCNT_TX_PMAC_SZ_512_1023];
+ r->hist_tx[5] = c[SCNT_TX_SZ_1024_1526] +
+ c[SCNT_TX_PMAC_SZ_1024_1526];
+ r->hist_tx[6] = c[SCNT_TX_SZ_JUMBO] +
+ c[SCNT_TX_PMAC_SZ_JUMBO];
+ break;
+ }
+
+ spin_unlock(&s->sw_lock);
+
+ *ranges = lan9645x_rmon_ranges;
+}
+
+/* Called in atomic context */
+void lan9645x_stats_get_stats64(struct lan9645x *lan9645x, int port,
+ struct rtnl_link_stats64 *stats)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = STAT_COUNTERS(lan9645x, LAN9645X_STAT_PORTS, port);
+
+ spin_lock(&s->sw_lock);
+
+ stats->rx_bytes = c[SCNT_RX_OCT] + c[SCNT_RX_PMAC_OCT];
+
+ stats->rx_packets = c[SCNT_RX_SHORT] +
+ c[SCNT_RX_FRAG] +
+ c[SCNT_RX_JABBER] +
+ c[SCNT_RX_CRC] +
+ c[SCNT_RX_SYMBOL_ERR] +
+ c[SCNT_RX_SZ_64] +
+ c[SCNT_RX_SZ_65_127] +
+ c[SCNT_RX_SZ_128_255] +
+ c[SCNT_RX_SZ_256_511] +
+ c[SCNT_RX_SZ_512_1023] +
+ c[SCNT_RX_SZ_1024_1526] +
+ c[SCNT_RX_SZ_JUMBO] +
+ c[SCNT_RX_LONG] +
+ c[SCNT_RX_PMAC_SHORT] +
+ c[SCNT_RX_PMAC_FRAG] +
+ c[SCNT_RX_PMAC_JABBER] +
+ c[SCNT_RX_PMAC_SZ_64] +
+ c[SCNT_RX_PMAC_SZ_65_127] +
+ c[SCNT_RX_PMAC_SZ_128_255] +
+ c[SCNT_RX_PMAC_SZ_256_511] +
+ c[SCNT_RX_PMAC_SZ_512_1023] +
+ c[SCNT_RX_PMAC_SZ_1024_1526] +
+ c[SCNT_RX_PMAC_SZ_JUMBO];
+
+ stats->multicast = c[SCNT_RX_MC] + c[SCNT_RX_PMAC_MC];
+
+ stats->rx_errors = c[SCNT_RX_SHORT] +
+ c[SCNT_RX_FRAG] +
+ c[SCNT_RX_JABBER] +
+ c[SCNT_RX_CRC] +
+ c[SCNT_RX_SYMBOL_ERR] +
+ c[SCNT_RX_LONG] +
+ c[SCNT_RX_PMAC_SHORT] +
+ c[SCNT_RX_PMAC_FRAG] +
+ c[SCNT_RX_PMAC_JABBER] +
+ c[SCNT_RX_PMAC_CRC] +
+ c[SCNT_RX_PMAC_SYMBOL_ERR] +
+ c[SCNT_RX_PMAC_LONG];
+
+ stats->rx_dropped = c[SCNT_RX_LONG] +
+ c[SCNT_DR_LOCAL] +
+ c[SCNT_DR_TAIL] +
+ c[SCNT_RX_CAT_DROP] +
+ c[SCNT_RX_RED_PRIO_0] +
+ c[SCNT_RX_RED_PRIO_1] +
+ c[SCNT_RX_RED_PRIO_2] +
+ c[SCNT_RX_RED_PRIO_3] +
+ c[SCNT_RX_RED_PRIO_4] +
+ c[SCNT_RX_RED_PRIO_5] +
+ c[SCNT_RX_RED_PRIO_6] +
+ c[SCNT_RX_RED_PRIO_7];
+
+ for (int i = 0; i < LAN9645X_NUM_TC; i++) {
+ stats->rx_dropped += c[SCNT_DR_YELLOW_PRIO_0 + i] +
+ c[SCNT_DR_GREEN_PRIO_0 + i];
+ }
+
+ stats->tx_bytes = c[SCNT_TX_OCT] + c[SCNT_TX_PMAC_OCT];
+
+ stats->tx_packets = c[SCNT_TX_SZ_64] +
+ c[SCNT_TX_SZ_65_127] +
+ c[SCNT_TX_SZ_128_255] +
+ c[SCNT_TX_SZ_256_511] +
+ c[SCNT_TX_SZ_512_1023] +
+ c[SCNT_TX_SZ_1024_1526] +
+ c[SCNT_TX_SZ_JUMBO] +
+ c[SCNT_TX_PMAC_SZ_64] +
+ c[SCNT_TX_PMAC_SZ_65_127] +
+ c[SCNT_TX_PMAC_SZ_128_255] +
+ c[SCNT_TX_PMAC_SZ_256_511] +
+ c[SCNT_TX_PMAC_SZ_512_1023] +
+ c[SCNT_TX_PMAC_SZ_1024_1526] +
+ c[SCNT_TX_PMAC_SZ_JUMBO];
+
+ stats->tx_dropped = c[SCNT_TX_DROP] + c[SCNT_TX_AGED];
+
+ stats->collisions = c[SCNT_TX_COL];
+
+ spin_unlock(&s->sw_lock);
+}
+
+void lan9645x_stats_get_eth_phy_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_eth_phy_stats *p)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = lan9645x_stats_port_update(lan9645x, port);
+
+ spin_lock(&s->sw_lock);
+
+ switch (p->src) {
+ case ETHTOOL_MAC_STATS_SRC_EMAC:
+ p->SymbolErrorDuringCarrier = c[SCNT_RX_SYMBOL_ERR];
+ break;
+ case ETHTOOL_MAC_STATS_SRC_PMAC:
+ p->SymbolErrorDuringCarrier = c[SCNT_RX_PMAC_SYMBOL_ERR];
+ break;
+ default:
+ p->SymbolErrorDuringCarrier = c[SCNT_RX_SYMBOL_ERR] +
+ c[SCNT_RX_PMAC_SYMBOL_ERR];
+ break;
+ }
+
+ spin_unlock(&s->sw_lock);
+}
+
+void
+lan9645x_stats_get_eth_ctrl_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_eth_ctrl_stats *ctrl)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = lan9645x_stats_port_update(lan9645x, port);
+
+ spin_lock(&s->sw_lock);
+
+ switch (ctrl->src) {
+ case ETHTOOL_MAC_STATS_SRC_EMAC:
+ ctrl->MACControlFramesReceived = c[SCNT_RX_CONTROL];
+ break;
+ case ETHTOOL_MAC_STATS_SRC_PMAC:
+ ctrl->MACControlFramesReceived = c[SCNT_RX_PMAC_CONTROL];
+ break;
+ default:
+ ctrl->MACControlFramesReceived = c[SCNT_RX_CONTROL] +
+ c[SCNT_RX_PMAC_CONTROL];
+ break;
+ }
+
+ spin_unlock(&s->sw_lock);
+}
+
+void lan9645x_stats_get_pause_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_pause_stats *ps)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = lan9645x_stats_port_update(lan9645x, port);
+
+ spin_lock(&s->sw_lock);
+
+ switch (ps->src) {
+ case ETHTOOL_MAC_STATS_SRC_EMAC:
+ ps->tx_pause_frames = c[SCNT_TX_PAUSE];
+ ps->rx_pause_frames = c[SCNT_RX_PAUSE];
+ break;
+ case ETHTOOL_MAC_STATS_SRC_PMAC:
+ ps->tx_pause_frames = c[SCNT_TX_PMAC_PAUSE];
+ ps->rx_pause_frames = c[SCNT_RX_PMAC_PAUSE];
+ break;
+ default:
+ ps->tx_pause_frames = c[SCNT_TX_PAUSE] + c[SCNT_TX_PMAC_PAUSE];
+ ps->rx_pause_frames = c[SCNT_RX_PAUSE] + c[SCNT_RX_PMAC_PAUSE];
+ break;
+ }
+
+ spin_unlock(&s->sw_lock);
+}
+
+void lan9645x_stats_get_mm_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_mm_stats *stats)
+{
+ struct lan9645x_stats *s = lan9645x->stats;
+ u64 *c;
+
+ c = lan9645x_stats_port_update(lan9645x, port);
+
+ spin_lock(&s->sw_lock);
+
+ stats->MACMergeFrameAssErrorCount = c[SCNT_RX_ASSEMBLY_ERR];
+ stats->MACMergeFrameSmdErrorCount = c[SCNT_RX_SMD_ERR];
+ stats->MACMergeFrameAssOkCount = c[SCNT_RX_ASSEMBLY_OK];
+ stats->MACMergeFragCountRx = c[SCNT_RX_MERGE_FRAG];
+ stats->MACMergeFragCountTx = c[SCNT_TX_MERGE_FRAG];
+ stats->MACMergeHoldCount = c[SCNT_TX_MM_HOLD];
+
+ spin_unlock(&s->sw_lock);
+}
+
+static void lan9645x_check_stats_work(struct work_struct *work)
+{
+ struct delayed_work *del_work = to_delayed_work(work);
+ struct lan9645x_stats *stats;
+
+ stats = container_of(del_work, struct lan9645x_stats, work);
+
+ lan9645x_stats_update(stats->lan9645x);
+
+ queue_delayed_work(stats->queue, &stats->work,
+ LAN9645X_STATS_CHECK_DELAY);
+}
+
+static int lan9645x_stats_prepare_regions(struct lan9645x *lan9645x,
+ struct lan9645x_view_stats *vstat)
+{
+ struct lan9645x_stat_region *regions;
+ const u32 *layout = vstat->layout;
+ size_t num_regions = 1;
+ int i;
+
+ for (i = 1; i < vstat->num_cnts; i++)
+ if (layout[i] != layout[i - 1] + 1)
+ num_regions++;
+
+ regions = devm_kcalloc(lan9645x->dev, num_regions, sizeof(*regions),
+ GFP_KERNEL);
+ if (!regions)
+ return -ENOMEM;
+
+ vstat->num_regions = num_regions;
+ vstat->regions = regions;
+
+ regions[0].base_offset = layout[0];
+ regions[0].cnts_base_idx = 0;
+ regions[0].cnt = 1;
+
+ for (i = 1, num_regions = 0; i < vstat->num_cnts; i++) {
+ if (layout[i] != layout[i - 1] + 1) {
+ num_regions++;
+ regions[num_regions].base_offset = layout[i];
+ regions[num_regions].cnts_base_idx = i;
+ regions[num_regions].cnt = 1;
+ } else {
+ regions[num_regions].cnt++;
+ }
+ }
+
+ return 0;
+}
+
+static int lan9645x_view_stat_init(struct lan9645x *lan9645x,
+ struct lan9645x_view_stats *vstat,
+ const struct lan9645x_view_stats *cfg)
+{
+ size_t total = cfg->num_cnts * cfg->num_indexes;
+ int err;
+
+ memcpy(vstat, cfg, sizeof(*cfg));
+
+ vstat->cnts = devm_kcalloc(lan9645x->dev, total, sizeof(u64),
+ GFP_KERNEL);
+ if (!vstat->cnts)
+ return -ENOMEM;
+
+ vstat->buf = devm_kcalloc(lan9645x->dev, total, sizeof(u32),
+ GFP_KERNEL);
+ if (!vstat->buf)
+ return -ENOMEM;
+
+ err = lan9645x_stats_prepare_regions(lan9645x, vstat);
+ if (err)
+ return err;
+
+ vstat->stats = lan9645x->stats;
+
+ return 0;
+}
+
+int lan9645x_stats_init(struct lan9645x *lan9645x)
+{
+ const struct lan9645x_view_stats *vs;
+ struct lan9645x_stats *stats;
+ int err, i;
+
+ lan9645x->stats = devm_kzalloc(lan9645x->dev, sizeof(*stats),
+ GFP_KERNEL);
+ if (!lan9645x->stats)
+ return -ENOMEM;
+
+ stats = lan9645x->stats;
+ stats->lan9645x = lan9645x;
+
+ mutex_init(&stats->hw_lock);
+ spin_lock_init(&stats->sw_lock);
+
+ for (i = 0; i < ARRAY_SIZE(lan9645x_view_stat_cfgs); i++) {
+ vs = &lan9645x_view_stat_cfgs[i];
+
+ if (!vs->num_cnts)
+ continue;
+
+ err = lan9645x_view_stat_init(lan9645x, &stats->view[vs->type],
+ vs);
+ if (err)
+ return err;
+ }
+
+ stats->queue = alloc_ordered_workqueue("%s-stats", 0,
+ dev_name(lan9645x->dev));
+ if (!stats->queue)
+ return -ENOMEM;
+
+ INIT_DELAYED_WORK(&stats->work, lan9645x_check_stats_work);
+ queue_delayed_work(stats->queue, &stats->work,
+ LAN9645X_STATS_CHECK_DELAY);
+
+ return 0;
+}
+
+void lan9645x_stats_deinit(struct lan9645x *lan9645x)
+{
+ cancel_delayed_work_sync(&lan9645x->stats->work);
+ destroy_workqueue(lan9645x->stats->queue);
+ mutex_destroy(&lan9645x->stats->hw_lock);
+ lan9645x->stats->queue = NULL;
+}
diff --git a/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.h b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.h
new file mode 100644
index 000000000000..268f6ad18088
--- /dev/null
+++ b/drivers/net/dsa/microchip/lan9645x/lan9645x_stats.h
@@ -0,0 +1,277 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright (C) 2026 Microchip Technology Inc.
+ */
+
+#ifndef _LAN9645X_STATS_H_
+#define _LAN9645X_STATS_H_
+
+#include "lan9645x_main.h"
+
+#define STATS_INDEX(vstats, idx) (&(vstats)->cnts[(vstats)->num_cnts * (idx)])
+
+#define STAT_COUNTERS(lan9645x, type, idx) \
+ STATS_INDEX(lan9645x_get_vstats(lan9645x, type), idx)
+
+/* Counter indices into stat layout structs */
+#define SCNT_FRER_SID_IN_PKT 0
+#define SCNT_ISDX_GREEN_OCT 1
+#define SCNT_ISDX_GREEN_PKT 2
+#define SCNT_ISDX_YELLOW_OCT 3
+#define SCNT_ISDX_YELLOW_PKT 4
+#define SCNT_ISDX_RED_OCT 5
+#define SCNT_ISDX_RED_PKT 6
+#define SCNT_ISDX_DROP_GREEN_OCT 7
+#define SCNT_ISDX_DROP_GREEN_PKT 8
+#define SCNT_ISDX_DROP_YELLOW_OCT 9
+#define SCNT_ISDX_DROP_YELLOW_PKT 10
+
+#define SCNT_SF_MATCHING_FRAMES_COUNT 0
+#define SCNT_SF_NOT_PASSING_FRAMES_COUNT 1
+#define SCNT_SF_NOT_PASSING_SDU_COUNT 2
+#define SCNT_SF_RED_FRAMES_COUNT 3
+#define SCNT_SF_STREAM_BLOCK_COUNT 4
+
+#define SCNT_ESDX_GREEN_OCT 0
+#define SCNT_ESDX_GREEN_PKT 1
+#define SCNT_ESDX_YELLOW_OCT 2
+#define SCNT_ESDX_YELLOW_PKT 3
+
+#define SCNT_RX_OCT 0
+#define SCNT_RX_UC 1
+#define SCNT_RX_MC 2
+#define SCNT_RX_BC 3
+#define SCNT_RX_SHORT 4
+#define SCNT_RX_FRAG 5
+#define SCNT_RX_JABBER 6
+#define SCNT_RX_CRC 7
+#define SCNT_RX_SYMBOL_ERR 8
+#define SCNT_RX_SZ_64 9
+#define SCNT_RX_SZ_65_127 10
+#define SCNT_RX_SZ_128_255 11
+#define SCNT_RX_SZ_256_511 12
+#define SCNT_RX_SZ_512_1023 13
+#define SCNT_RX_SZ_1024_1526 14
+#define SCNT_RX_SZ_JUMBO 15
+#define SCNT_RX_PAUSE 16
+#define SCNT_RX_CONTROL 17
+#define SCNT_RX_LONG 18
+#define SCNT_RX_CAT_DROP 19
+#define SCNT_RX_RED_PRIO_0 20
+#define SCNT_RX_RED_PRIO_1 21
+#define SCNT_RX_RED_PRIO_2 22
+#define SCNT_RX_RED_PRIO_3 23
+#define SCNT_RX_RED_PRIO_4 24
+#define SCNT_RX_RED_PRIO_5 25
+#define SCNT_RX_RED_PRIO_6 26
+#define SCNT_RX_RED_PRIO_7 27
+#define SCNT_RX_YELLOW_PRIO_0 28
+#define SCNT_RX_YELLOW_PRIO_1 29
+#define SCNT_RX_YELLOW_PRIO_2 30
+#define SCNT_RX_YELLOW_PRIO_3 31
+#define SCNT_RX_YELLOW_PRIO_4 32
+#define SCNT_RX_YELLOW_PRIO_5 33
+#define SCNT_RX_YELLOW_PRIO_6 34
+#define SCNT_RX_YELLOW_PRIO_7 35
+#define SCNT_RX_GREEN_PRIO_0 36
+#define SCNT_RX_GREEN_PRIO_1 37
+#define SCNT_RX_GREEN_PRIO_2 38
+#define SCNT_RX_GREEN_PRIO_3 39
+#define SCNT_RX_GREEN_PRIO_4 40
+#define SCNT_RX_GREEN_PRIO_5 41
+#define SCNT_RX_GREEN_PRIO_6 42
+#define SCNT_RX_GREEN_PRIO_7 43
+#define SCNT_RX_ASSEMBLY_ERR 44
+#define SCNT_RX_SMD_ERR 45
+#define SCNT_RX_ASSEMBLY_OK 46
+#define SCNT_RX_MERGE_FRAG 47
+#define SCNT_RX_PMAC_OCT 48
+#define SCNT_RX_PMAC_UC 49
+#define SCNT_RX_PMAC_MC 50
+#define SCNT_RX_PMAC_BC 51
+#define SCNT_RX_PMAC_SHORT 52
+#define SCNT_RX_PMAC_FRAG 53
+#define SCNT_RX_PMAC_JABBER 54
+#define SCNT_RX_PMAC_CRC 55
+#define SCNT_RX_PMAC_SYMBOL_ERR 56
+#define SCNT_RX_PMAC_SZ_64 57
+#define SCNT_RX_PMAC_SZ_65_127 58
+#define SCNT_RX_PMAC_SZ_128_255 59
+#define SCNT_RX_PMAC_SZ_256_511 60
+#define SCNT_RX_PMAC_SZ_512_1023 61
+#define SCNT_RX_PMAC_SZ_1024_1526 62
+#define SCNT_RX_PMAC_SZ_JUMBO 63
+#define SCNT_RX_PMAC_PAUSE 64
+#define SCNT_RX_PMAC_CONTROL 65
+#define SCNT_RX_PMAC_LONG 66
+#define SCNT_TX_OCT 67
+#define SCNT_TX_UC 68
+#define SCNT_TX_MC 69
+#define SCNT_TX_BC 70
+#define SCNT_TX_COL 71
+#define SCNT_TX_DROP 72
+#define SCNT_TX_PAUSE 73
+#define SCNT_TX_SZ_64 74
+#define SCNT_TX_SZ_65_127 75
+#define SCNT_TX_SZ_128_255 76
+#define SCNT_TX_SZ_256_511 77
+#define SCNT_TX_SZ_512_1023 78
+#define SCNT_TX_SZ_1024_1526 79
+#define SCNT_TX_SZ_JUMBO 80
+#define SCNT_TX_YELLOW_PRIO_0 81
+#define SCNT_TX_YELLOW_PRIO_1 82
+#define SCNT_TX_YELLOW_PRIO_2 83
+#define SCNT_TX_YELLOW_PRIO_3 84
+#define SCNT_TX_YELLOW_PRIO_4 85
+#define SCNT_TX_YELLOW_PRIO_5 86
+#define SCNT_TX_YELLOW_PRIO_6 87
+#define SCNT_TX_YELLOW_PRIO_7 88
+#define SCNT_TX_GREEN_PRIO_0 89
+#define SCNT_TX_GREEN_PRIO_1 90
+#define SCNT_TX_GREEN_PRIO_2 91
+#define SCNT_TX_GREEN_PRIO_3 92
+#define SCNT_TX_GREEN_PRIO_4 93
+#define SCNT_TX_GREEN_PRIO_5 94
+#define SCNT_TX_GREEN_PRIO_6 95
+#define SCNT_TX_GREEN_PRIO_7 96
+#define SCNT_TX_AGED 97
+#define SCNT_TX_LLCT 98
+#define SCNT_TX_CT 99
+#define SCNT_TX_BUFDROP 100
+#define SCNT_TX_MM_HOLD 101
+#define SCNT_TX_MERGE_FRAG 102
+#define SCNT_TX_PMAC_OCT 103
+#define SCNT_TX_PMAC_UC 104
+#define SCNT_TX_PMAC_MC 105
+#define SCNT_TX_PMAC_BC 106
+#define SCNT_TX_PMAC_PAUSE 107
+#define SCNT_TX_PMAC_SZ_64 108
+#define SCNT_TX_PMAC_SZ_65_127 109
+#define SCNT_TX_PMAC_SZ_128_255 110
+#define SCNT_TX_PMAC_SZ_256_511 111
+#define SCNT_TX_PMAC_SZ_512_1023 112
+#define SCNT_TX_PMAC_SZ_1024_1526 113
+#define SCNT_TX_PMAC_SZ_JUMBO 114
+#define SCNT_DR_LOCAL 115
+#define SCNT_DR_TAIL 116
+#define SCNT_DR_YELLOW_PRIO_0 117
+#define SCNT_DR_YELLOW_PRIO_1 118
+#define SCNT_DR_YELLOW_PRIO_2 119
+#define SCNT_DR_YELLOW_PRIO_3 120
+#define SCNT_DR_YELLOW_PRIO_4 121
+#define SCNT_DR_YELLOW_PRIO_5 122
+#define SCNT_DR_YELLOW_PRIO_6 123
+#define SCNT_DR_YELLOW_PRIO_7 124
+#define SCNT_DR_GREEN_PRIO_0 125
+#define SCNT_DR_GREEN_PRIO_1 126
+#define SCNT_DR_GREEN_PRIO_2 127
+#define SCNT_DR_GREEN_PRIO_3 128
+#define SCNT_DR_GREEN_PRIO_4 129
+#define SCNT_DR_GREEN_PRIO_5 130
+#define SCNT_DR_GREEN_PRIO_6 131
+#define SCNT_DR_GREEN_PRIO_7 132
+
+enum lan9645x_view_stat_type {
+ LAN9645X_STAT_PORTS = 0,
+ LAN9645X_STAT_ISDX,
+ LAN9645X_STAT_ESDX,
+ LAN9645X_STAT_SFID,
+
+ LAN9645X_STAT_NUM,
+};
+
+struct lan9645x_stat_region {
+ u32 base_offset;
+ u32 cnt;
+ u32 cnts_base_idx;
+};
+
+/* Counters are organized by indices/views such as
+ *
+ * - physical ports
+ * - isdx
+ * - esdx
+ * - frer
+ * - sfid
+ *
+ * Each view contains regions, which is a linear address range of related
+ * stats. I.e. the ports index has RX, TX and Drop regions.
+ *
+ *
+ * and you have a given counter replicated per index.
+ */
+struct lan9645x_view_stats {
+ /* HW register offsets indexed by SCNT_*, used for bulk reading */
+ const u32 *layout;
+ /* Region description for this view, used for bulk reading */
+ struct lan9645x_stat_region *regions;
+ struct lan9645x_stats *stats;
+ char name[16];
+ /* 64bit software counters with the same addr layout hw */
+ u64 *cnts;
+ /* Buffer for bulk reading counter regions from hw */
+ u32 *buf;
+ /* Number of counters per index in view */
+ u32 num_cnts;
+ /* Number of indexes in view */
+ u32 num_indexes;
+ /* Number of counter regions with counters at sequential addresses */
+ size_t num_regions;
+ enum lan9645x_view_stat_type type;
+};
+
+struct lan9645x_stats {
+ struct lan9645x *lan9645x;
+ struct mutex hw_lock; /* lock r/w to stat registers and u32 buf */
+ spinlock_t sw_lock; /* lock access to u64 software counters */
+ struct delayed_work work;
+ struct workqueue_struct *queue;
+
+ struct lan9645x_view_stats view[LAN9645X_STAT_NUM];
+};
+
+static inline struct lan9645x_view_stats *
+lan9645x_get_vstats(struct lan9645x *lan9645x,
+ enum lan9645x_view_stat_type type)
+{
+ if (WARN_ON(!(type < LAN9645X_STAT_NUM)))
+ return NULL;
+
+ return &lan9645x->stats->view[type];
+}
+
+/* Add a possibly wrapping 32 bit value to a 64 bit counter */
+static inline void lan9645x_stats_add_cnt(u64 *cnt, u32 val)
+{
+ if (val < (*cnt & U32_MAX))
+ *cnt += (u64)1 << 32; /* value has wrapped */
+
+ *cnt = (*cnt & ~(u64)U32_MAX) + val;
+}
+
+int lan9645x_stats_init(struct lan9645x *lan9645x);
+void lan9645x_stats_deinit(struct lan9645x *lan9645x);
+void lan9645x_stats_get_strings(struct lan9645x *lan9645x, int port,
+ u32 stringset, u8 *data);
+int lan9645x_stats_get_sset_count(struct lan9645x *lan9645x, int port,
+ int sset);
+void lan9645x_stats_get_ethtool_stats(struct lan9645x *lan9645x, int port,
+ uint64_t *data);
+void lan9645x_stats_get_eth_mac_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_eth_mac_stats *mac_stats);
+void
+lan9645x_stats_get_rmon_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_rmon_stats *rmon_stats,
+ const struct ethtool_rmon_hist_range **ranges);
+void lan9645x_stats_get_stats64(struct lan9645x *lan9645x, int port,
+ struct rtnl_link_stats64 *s);
+void lan9645x_stats_get_mm_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_mm_stats *stats);
+void lan9645x_stats_get_pause_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_pause_stats *ps);
+void
+lan9645x_stats_get_eth_ctrl_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_eth_ctrl_stats *ctrl_stats);
+void lan9645x_stats_get_eth_phy_stats(struct lan9645x *lan9645x, int port,
+ struct ethtool_eth_phy_stats *phy_stats);
+
+#endif
--
2.52.0
^ permalink raw reply related
* Re: [PATCH v2 net-next 00/15] net/sched: prepare RTNL removal from qdisc dumps
From: Jamal Hadi Salim @ 2026-04-10 11:49 UTC (permalink / raw)
To: Eric Dumazet
Cc: David S . Miller, Jakub Kicinski, Paolo Abeni, Simon Horman,
Jiri Pirko, Kuniyuki Iwashima, netdev, eric.dumazet
In-Reply-To: <20260409214914.3072827-1-edumazet@google.com>
On Thu, Apr 9, 2026 at 5:49 PM Eric Dumazet <edumazet@google.com> wrote:
>
> We add annotations for data-races, so that most dump methods
> can run in parallel with data path.
>
> Then change mq and mqprio to no longer acquire each children
> qdisc spinlock.
>
> Next round of patches will wait for linux-7.2.
Reviewed-by: Jamal Hadi Salim <jhs@mojatatu.com>
with a small caveat: Shouldnt patch #6 onwards go to net?
cheers,
jamal
^ 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