Ethernet Bridge development
 help / color / mirror / Atom feed
* [PATCH net-next v2 0/2] net: dsa: fix host fdb refcount when an entry roams
@ 2026-11-14 22:53 Julius Bairaktaris
  2026-11-14 22:53 ` [PATCH net-next v2 1/2] net: bridge: switchdev: add br_switchdev_fdb_find_port() Julius Bairaktaris
  2026-11-14 22:53 ` [PATCH net-next v2 2/2] net: dsa: fix host fdb refcount when a bridge fdb entry roams Julius Bairaktaris
  0 siblings, 2 replies; 3+ messages in thread
From: Julius Bairaktaris @ 2026-11-14 22:53 UTC (permalink / raw)
  To: Nikolay Aleksandrov, Ido Schimmel, Andrew Lunn, Vladimir Oltean
  Cc: netdev, bridge, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, linux-kernel

When a bridge FDB entry roams between ports, br_fdb_update() notifies
switchdev of the deletion and the addition without a lock. With two
CPUs learning one address on different ports, DSA counts the host
address on its CPU port more or fewer times than the bridge has it,
which ends in "failed to delete ... from fdb: -2" or an address left on
the CPU port.

The earlier attempt took hash_lock in br_fdb_update(); Nikolay rejected
it because it slows down learning and roaming. This series leaves
br_fdb_update() and the rest of the bridge's learning path unchanged
and fixes DSA instead: each user port records the host addresses it
holds and, on every event, checks where the bridge has the entry now.
Patch 1 is the only bridge change: an RCU lookup that DSA calls from
its workqueue. Patch 2 replaces the counting.

The counting has been exposed to the race since commit 90dc8fd36078
("net: bridge: notify switchdev of disappearance of old FDB entry upon
migration"). The change redesigns how DSA tracks host addresses, so it
goes to net-next without a Fixes: tag.

Tested in QEMU with dsa_loop given FDB operations and assisted
learning, PROVE_LOCKING, PROVE_RCU and DEBUG_ATOMIC_SLEEP enabled, one
source address sent from two veth ports on two CPUs for 5 s, 5 boots
per kernel: net-next logs the -2 error and leaves the address on the
CPU port in 4 of 5 boots, this series in none, and it removes the
address when the user ports leave the bridge. A "bridge fdb replace" of
the address onto a user port leaves it on the CPU port in 5 of 5 boots
on net-next and removes it in 5 of 5 with this series. In a second
test the N-th allocation of "ip link set lan1 master br0" fails for
N = 1 to 150 while the bridge has 40 FDB entries to replay: v1 oopses
in dsa_user_switchdev_event_work() in 2 of 2 boots, this version in
none of 5 boots, and no address stays on the CPU port. The kernel log
of every boot has no lockdep, RCU or atomic sleep reports.

Changes in v2 (Sashiko's review of v1):
- An addition on a user port releases the address on every user port
  of the bridge, also of another tree, and before the entry is
  installed: the release no longer walks one tree, and no longer erases
  the entry on switches that keep one entry per address.
- Local entries use the same lookup, so a "bridge fdb replace" between
  a local and a foreign entry leaves no address behind.
- The lookup applies assisted_learning_on_cpu_port, and reports whether
  the entry was added by the user.
- The release is scheduled before the lag_fdb_add check, and only for
  ports that hold an address for a foreign entry when a learned entry
  moved.
- An entry is not reported as offloaded when its host address could
  not be added; the conduit's unicast reference is undone when the
  switch add fails, and a failed delete drops its record instead of
  repeating the conduit delete.
- Queued events no longer dereference dp->bridge after a failed join:
  they read it once and drop themselves, the FDB helpers take the
  bridge from the caller, and dp->bridge is accessed with
  READ_ONCE()/WRITE_ONCE().
- Patch 1: kernel-doc and message state what the lookup returns for
  local, locked, "master dynamic" and driver-learned entries, and name
  the DSA caller.

Changes since the hash_lock patch:
- Leave br_fdb_update() unchanged (Nikolay).
- Sashiko's finding on that patch, a deletion that races the locked
  roam, has no counterpart here: DSA looks the entry up when it handles
  each event and treats a deleted entry as gone.
- Of the three findings it lists as pre-existing, the MAB and
  BR_FDB_LOCAL ones are in the bridge, which this series does not
  change; the peer-site one, a "bridge fdb replace" onto a DSA user port
  that notifies no deletion, is handled in patch 2.

Link: https://lore.kernel.org/netdev/20261004124254.3525496-1-julius@bairaktaris.de/
Link: https://lore.kernel.org/netdev/cover.1794134495.git.julius@bairaktaris.de/

I used Claude (Anthropic) to help write these patches.

Julius Bairaktaris (2):
  net: bridge: switchdev: add br_switchdev_fdb_find_port()
  net: dsa: fix host fdb refcount when a bridge fdb entry roams

 include/linux/if_bridge.h |  14 ++
 include/net/dsa.h         |   4 +-
 net/bridge/br_switchdev.c |  72 +++++++-
 net/dsa/port.c            |  50 ++++--
 net/dsa/port.h            |  26 +--
 net/dsa/user.c            | 353 +++++++++++++++++++++++++++++++++++---
 net/dsa/user.h            |  22 +++
 7 files changed, 473 insertions(+), 68 deletions(-)


base-commit: 6964ff21c5d8b75649211a52dbfd92753e605649
-- 
2.53.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH net-next v2 1/2] net: bridge: switchdev: add br_switchdev_fdb_find_port()
  2026-11-14 22:53 [PATCH net-next v2 0/2] net: dsa: fix host fdb refcount when an entry roams Julius Bairaktaris
@ 2026-11-14 22:53 ` Julius Bairaktaris
  2026-11-14 22:53 ` [PATCH net-next v2 2/2] net: dsa: fix host fdb refcount when a bridge fdb entry roams Julius Bairaktaris
  1 sibling, 0 replies; 3+ messages in thread
From: Julius Bairaktaris @ 2026-11-14 22:53 UTC (permalink / raw)
  To: Nikolay Aleksandrov, Ido Schimmel, Andrew Lunn, Vladimir Oltean
  Cc: netdev, bridge, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, linux-kernel

Add an RCU lookup that returns the device br_switchdev_fdb_notify()
reports an FDB entry on: its port, or the bridge for a local entry. It
does not find the entries whose addition the bridge does not notify to
switchdev: locked entries, entries the user added as "master dynamic",
and entries a switchdev driver learned.

A switchdev driver that handles FDB events from a workqueue cannot take
RTNL for br_fdb_find_port(), and has to look up where the bridge has an
entry when it handles an event. The next patch uses the helper for this
in DSA. The filter of br_switchdev_fdb_notify() moves into a helper that
both use, with no functional change; the learning path does not change.

Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
---
Changes in v2:
- Kernel-doc and message state what the lookup returns: the bridge for
  a local entry, the port otherwise, nothing for locked, "master
  dynamic" and driver-learned entries.
- Report whether the entry was added by the user.
- The message names the DSA caller.

 include/linux/if_bridge.h | 14 ++++++++
 net/bridge/br_switchdev.c | 72 ++++++++++++++++++++++++++++++++++-----
 2 files changed, 77 insertions(+), 9 deletions(-)

diff --git a/include/linux/if_bridge.h b/include/linux/if_bridge.h
index 75673b8bffcb8..027147d618a4e 100644
--- a/include/linux/if_bridge.h
+++ b/include/linux/if_bridge.h
@@ -201,6 +201,20 @@ static inline int br_mst_get_state(const struct net_device *dev, u16 msti,
 }
 #endif
 
+#if IS_ENABLED(CONFIG_BRIDGE) && IS_ENABLED(CONFIG_NET_SWITCHDEV)
+struct net_device *br_switchdev_fdb_find_port(struct net_device *br_dev,
+					      const unsigned char *addr,
+					      u16 vid, bool *added_by_user);
+#else
+static inline struct net_device *
+br_switchdev_fdb_find_port(struct net_device *br_dev,
+			   const unsigned char *addr, u16 vid,
+			   bool *added_by_user)
+{
+	return NULL;
+}
+#endif
+
 #if IS_ENABLED(CONFIG_BRIDGE)
 struct net_device *br_fdb_find_port(const struct net_device *br_dev,
 				    const unsigned char *addr,
diff --git a/net/bridge/br_switchdev.c b/net/bridge/br_switchdev.c
index 10f3aea6a1f82..f832657c0c22e 100644
--- a/net/bridge/br_switchdev.c
+++ b/net/bridge/br_switchdev.c
@@ -141,14 +141,10 @@ static void br_switchdev_fdb_populate(struct net_bridge *br,
 	item->info.ctx = ctx;
 }
 
-void
-br_switchdev_fdb_notify(struct net_bridge *br,
-			const struct net_bridge_fdb_entry *fdb, int type)
+static bool br_switchdev_fdb_notifies(const struct net_bridge_fdb_entry *fdb)
 {
-	struct switchdev_notifier_fdb_info item;
-
 	if (test_bit(BR_FDB_LOCKED, &fdb->flags))
-		return;
+		return false;
 
 	/* Entries with these flags were created using ndm_state == NUD_REACHABLE,
 	 * ndm_flags == NTF_MASTER( | NTF_STICKY), ext_flags == 0 by something
@@ -156,9 +152,67 @@ br_switchdev_fdb_notify(struct net_bridge *br,
 	 * Drivers don't know how to deal with these, so don't notify them to
 	 * avoid confusing them.
 	 */
-	if (test_bit(BR_FDB_ADDED_BY_USER, &fdb->flags) &&
-	    !test_bit(BR_FDB_STATIC, &fdb->flags) &&
-	    !test_bit(BR_FDB_ADDED_BY_EXT_LEARN, &fdb->flags))
+	return !test_bit(BR_FDB_ADDED_BY_USER, &fdb->flags) ||
+	       test_bit(BR_FDB_STATIC, &fdb->flags) ||
+	       test_bit(BR_FDB_ADDED_BY_EXT_LEARN, &fdb->flags);
+}
+
+/**
+ * br_switchdev_fdb_find_port - find the device a switchdev entry is reported on
+ * @br_dev: the bridge device
+ * @addr: the MAC address of the entry
+ * @vid: the VLAN of the entry
+ * @added_by_user: set to whether the entry was added by the user, may be NULL
+ *
+ * Must be called under RCU; the device is valid until the read-side critical
+ * section ends.
+ *
+ * Return: the device that br_switchdev_fdb_notify() reports the entry for
+ * @addr and @vid on: its port, or @br_dev for a local entry. NULL if there is
+ * no such entry, or if it is locked, was added with "master dynamic", or was
+ * learned by a switchdev driver, whose addition br_switchdev_fdb_notify()
+ * does not report.
+ */
+struct net_device *br_switchdev_fdb_find_port(struct net_device *br_dev,
+					      const unsigned char *addr,
+					      u16 vid, bool *added_by_user)
+{
+	const struct net_bridge_port *p;
+	struct net_bridge_fdb_entry *f;
+	bool by_user;
+
+	RCU_LOCKDEP_WARN(!rcu_read_lock_held(), "suspicious use outside RCU");
+
+	if (!netif_is_bridge_master(br_dev))
+		return NULL;
+
+	f = br_fdb_find_rcu(netdev_priv(br_dev), addr, vid);
+	if (!f || !br_switchdev_fdb_notifies(f))
+		return NULL;
+
+	by_user = test_bit(BR_FDB_ADDED_BY_USER, &f->flags);
+	if (!by_user && test_bit(BR_FDB_ADDED_BY_EXT_LEARN, &f->flags))
+		return NULL;
+
+	if (added_by_user)
+		*added_by_user = by_user;
+
+	if (test_bit(BR_FDB_LOCAL, &f->flags))
+		return br_dev;
+
+	p = br_fdb_dst_port(f);
+
+	return p ? p->dev : NULL;
+}
+EXPORT_SYMBOL_GPL(br_switchdev_fdb_find_port);
+
+void
+br_switchdev_fdb_notify(struct net_bridge *br,
+			const struct net_bridge_fdb_entry *fdb, int type)
+{
+	struct switchdev_notifier_fdb_info item;
+
+	if (!br_switchdev_fdb_notifies(fdb))
 		return;
 
 	br_switchdev_fdb_populate(br, &item, fdb, NULL);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* [PATCH net-next v2 2/2] net: dsa: fix host fdb refcount when a bridge fdb entry roams
  2026-11-14 22:53 [PATCH net-next v2 0/2] net: dsa: fix host fdb refcount when an entry roams Julius Bairaktaris
  2026-11-14 22:53 ` [PATCH net-next v2 1/2] net: bridge: switchdev: add br_switchdev_fdb_find_port() Julius Bairaktaris
@ 2026-11-14 22:53 ` Julius Bairaktaris
  1 sibling, 0 replies; 3+ messages in thread
From: Julius Bairaktaris @ 2026-11-14 22:53 UTC (permalink / raw)
  To: Nikolay Aleksandrov, Ido Schimmel, Andrew Lunn, Vladimir Oltean
  Cc: netdev, bridge, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, linux-kernel

A bridge FDB entry on a foreign port is a host address on every user
port in the bridge. When the entry roams, br_fdb_update() notifies the
deletion from the old port and the addition on the new one without a
lock. With two CPUs receiving the address on different ports, DSA can
get two deletions for one addition, an addition for a port the entry
already left, or the deletion before the addition. Counting these
events either makes a deletion find no reference left on the CPU port,
or leaks a reference, which keeps the address on the CPU port after the
entry moved to a user port or was deleted. The first ends in this
error, here from dsa_loop:

  port 1 failed to delete 02:5a:0b:a2:1a:46 vid 0 from fdb: -2

Instead of counting, each user port records the host addresses it
holds. On every event it holds the address while the bridge has the
entry on a foreign port and notifies switchdev of it (a learned entry
only with assisted learning), or has it as a local entry, and releases
it otherwise. A deletion that still finds the entry on its own port was
sent before the bridge changed the port, so the address is released.
Replayed deletions release it without the lookup.

An entry on a user port, also one of another tree, is
notified only to that port. An addition there releases the address on
every user port of the bridge that holds it, from the lower devices of
the bridge, and before the entry is installed: some switches keep one
entry per address, which a later release would erase. This also covers a
"bridge fdb replace" onto a user port, which notifies no deletion. A
failed host address add does not report the entry as offloaded and
undoes its unicast filter reference on the conduit; a failed delete
drops the record, so the conduit reference is not released twice.

FDB events can still be queued when a port leaves the bridge or fails to
join it. The events read dp->bridge once and are dropped when it is
gone, the FDB helpers take the bridge from the caller, and
dsa_port_bridge_destroy() clears dp->bridge before it flushes the
workqueue and releases the records, so the bridge is not freed under a
running event.

In QEMU, with dsa_loop given FDB operations and assisted learning, two
veth ports in its bridge and one source address sent from both on two
CPUs for 5 s, the deletion fails and the address stays on the CPU port
after both veth ports go down in 4 of 5 boots. With this change neither
happens in 5 of 5 boots, and the address is removed when the user ports
leave the bridge. A "bridge fdb replace" of the address onto a user port
leaves it on the CPU port in 5 of 5 boots without this change and
removes it in 5 of 5 with it. Replacing the entry with a local one and
back leaves no address behind. With 40 static entries to replay and the
N-th allocation of "ip link set lan1 master br0" failing for N = 1 to
150, the bridge join fails 73 to 77 times per boot; the previous version
oopses in dsa_user_switchdev_event_work() in 2 of 2 boots, this one
does not in 5 of 5 boots and leaves no address on the CPU port.

Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
---
Changes in v2:
- Release the address on every user port of the bridge, also of another
  tree, before the entry is installed.
- Hold addresses of local entries through the same lookup.
- Apply assisted_learning_on_cpu_port; schedule the release before the
  lag_fdb_add check and only for ports that hold a foreign address.
- No offloaded notification for a failed add; undo the conduit
  reference when the switch add fails; drop the record on a failed
  delete.
- Drop events that find no bridge after a failed join; pass the bridge
  to the FDB helpers; READ_ONCE()/WRITE_ONCE() on dp->bridge.

 include/net/dsa.h |   4 +-
 net/dsa/port.c    |  50 ++++---
 net/dsa/port.h    |  26 ++--
 net/dsa/user.c    | 353 ++++++++++++++++++++++++++++++++++++++++++----
 net/dsa/user.h    |  22 +++
 5 files changed, 396 insertions(+), 59 deletions(-)

diff --git a/include/net/dsa.h b/include/net/dsa.h
index 5d12191b6f6f5..6e63d88bafa60 100644
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -772,7 +772,9 @@ struct net_device *dsa_port_to_bridge_port(const struct dsa_port *dp)
 static inline struct net_device *
 dsa_port_bridge_dev_get(const struct dsa_port *dp)
 {
-	return dp->bridge ? dp->bridge->dev : NULL;
+	const struct dsa_bridge *bridge = READ_ONCE(dp->bridge);
+
+	return bridge ? bridge->dev : NULL;
 }
 
 static inline unsigned int dsa_port_bridge_num_get(struct dsa_port *dp)
diff --git a/net/dsa/port.c b/net/dsa/port.c
index 1f5536c0dffc2..1ec2c79387150 100644
--- a/net/dsa/port.c
+++ b/net/dsa/port.c
@@ -427,7 +427,7 @@ static int dsa_port_bridge_create(struct dsa_port *dp,
 	bridge = dsa_tree_bridge_find(ds->dst, br);
 	if (bridge) {
 		refcount_inc(&bridge->refcount);
-		dp->bridge = bridge;
+		WRITE_ONCE(dp->bridge, bridge);
 		return 0;
 	}
 
@@ -447,7 +447,7 @@ static int dsa_port_bridge_create(struct dsa_port *dp,
 		return -EOPNOTSUPP;
 	}
 
-	dp->bridge = bridge;
+	WRITE_ONCE(dp->bridge, bridge);
 
 	return 0;
 }
@@ -457,7 +457,13 @@ static void dsa_port_bridge_destroy(struct dsa_port *dp,
 {
 	struct dsa_bridge *bridge = dp->bridge;
 
-	dp->bridge = NULL;
+	WRITE_ONCE(dp->bridge, NULL);
+
+	/* Wait for the FDB events that still see the bridge, then release the
+	 * host addresses the port holds, before the drivers are told it left.
+	 */
+	dsa_flush_workqueue();
+	dsa_user_bridge_host_fdb_purge(dp, bridge);
 
 	if (!refcount_dec_and_test(&bridge->refcount))
 		return;
@@ -528,7 +534,9 @@ int dsa_port_bridge_join(struct dsa_port *dp, struct net_device *br,
 					&dsa_user_switchdev_blocking_notifier);
 	dsa_flush_workqueue();
 out_rollback_unbridge:
+	dsa_port_bridge_destroy(dp, br);
 	dsa_broadcast(DSA_NOTIFIER_BRIDGE_LEAVE, &info);
+	return err;
 out_rollback:
 	dsa_port_bridge_destroy(dp, br);
 	return err;
@@ -973,8 +981,8 @@ int dsa_port_mtu_change(struct dsa_port *dp, int new_mtu)
 	return dsa_port_notify(dp, DSA_NOTIFIER_MTU, &info);
 }
 
-int dsa_port_fdb_add(struct dsa_port *dp, const unsigned char *addr,
-		     u16 vid)
+int dsa_port_fdb_add(struct dsa_port *dp, const struct dsa_bridge *bridge,
+		     const unsigned char *addr, u16 vid)
 {
 	struct dsa_notifier_fdb_info info = {
 		.dp = dp,
@@ -982,7 +990,7 @@ int dsa_port_fdb_add(struct dsa_port *dp, const unsigned char *addr,
 		.vid = vid,
 		.db = {
 			.type = DSA_DB_BRIDGE,
-			.bridge = *dp->bridge,
+			.bridge = *bridge,
 		},
 	};
 
@@ -996,8 +1004,8 @@ int dsa_port_fdb_add(struct dsa_port *dp, const unsigned char *addr,
 	return dsa_port_notify(dp, DSA_NOTIFIER_FDB_ADD, &info);
 }
 
-int dsa_port_fdb_del(struct dsa_port *dp, const unsigned char *addr,
-		     u16 vid)
+int dsa_port_fdb_del(struct dsa_port *dp, const struct dsa_bridge *bridge,
+		     const unsigned char *addr, u16 vid)
 {
 	struct dsa_notifier_fdb_info info = {
 		.dp = dp,
@@ -1005,7 +1013,7 @@ int dsa_port_fdb_del(struct dsa_port *dp, const unsigned char *addr,
 		.vid = vid,
 		.db = {
 			.type = DSA_DB_BRIDGE,
-			.bridge = *dp->bridge,
+			.bridge = *bridge,
 		},
 	};
 
@@ -1041,12 +1049,13 @@ int dsa_port_standalone_host_fdb_add(struct dsa_port *dp,
 }
 
 int dsa_port_bridge_host_fdb_add(struct dsa_port *dp,
+				 const struct dsa_bridge *bridge,
 				 const unsigned char *addr, u16 vid)
 {
 	struct net_device *conduit = dsa_port_to_conduit(dp);
 	struct dsa_db db = {
 		.type = DSA_DB_BRIDGE,
-		.bridge = *dp->bridge,
+		.bridge = *bridge,
 	};
 	int err;
 
@@ -1063,7 +1072,11 @@ int dsa_port_bridge_host_fdb_add(struct dsa_port *dp,
 			return err;
 	}
 
-	return dsa_port_host_fdb_add(dp, addr, vid, db);
+	err = dsa_port_host_fdb_add(dp, addr, vid, db);
+	if (err && (conduit->priv_flags & IFF_UNICAST_FLT))
+		dev_uc_del(conduit, addr);
+
+	return err;
 }
 
 static int dsa_port_host_fdb_del(struct dsa_port *dp,
@@ -1092,12 +1105,13 @@ int dsa_port_standalone_host_fdb_del(struct dsa_port *dp,
 }
 
 int dsa_port_bridge_host_fdb_del(struct dsa_port *dp,
+				 const struct dsa_bridge *bridge,
 				 const unsigned char *addr, u16 vid)
 {
 	struct net_device *conduit = dsa_port_to_conduit(dp);
 	struct dsa_db db = {
 		.type = DSA_DB_BRIDGE,
-		.bridge = *dp->bridge,
+		.bridge = *bridge,
 	};
 	int err;
 
@@ -1113,8 +1127,8 @@ int dsa_port_bridge_host_fdb_del(struct dsa_port *dp,
 	return dsa_port_host_fdb_del(dp, addr, vid, db);
 }
 
-int dsa_port_lag_fdb_add(struct dsa_port *dp, const unsigned char *addr,
-			 u16 vid)
+int dsa_port_lag_fdb_add(struct dsa_port *dp, const struct dsa_bridge *bridge,
+			 const unsigned char *addr, u16 vid)
 {
 	struct dsa_notifier_lag_fdb_info info = {
 		.lag = dp->lag,
@@ -1122,7 +1136,7 @@ int dsa_port_lag_fdb_add(struct dsa_port *dp, const unsigned char *addr,
 		.vid = vid,
 		.db = {
 			.type = DSA_DB_BRIDGE,
-			.bridge = *dp->bridge,
+			.bridge = *bridge,
 		},
 	};
 
@@ -1132,8 +1146,8 @@ int dsa_port_lag_fdb_add(struct dsa_port *dp, const unsigned char *addr,
 	return dsa_port_notify(dp, DSA_NOTIFIER_LAG_FDB_ADD, &info);
 }
 
-int dsa_port_lag_fdb_del(struct dsa_port *dp, const unsigned char *addr,
-			 u16 vid)
+int dsa_port_lag_fdb_del(struct dsa_port *dp, const struct dsa_bridge *bridge,
+			 const unsigned char *addr, u16 vid)
 {
 	struct dsa_notifier_lag_fdb_info info = {
 		.lag = dp->lag,
@@ -1141,7 +1155,7 @@ int dsa_port_lag_fdb_del(struct dsa_port *dp, const unsigned char *addr,
 		.vid = vid,
 		.db = {
 			.type = DSA_DB_BRIDGE,
-			.bridge = *dp->bridge,
+			.bridge = *bridge,
 		},
 	};
 
diff --git a/net/dsa/port.h b/net/dsa/port.h
index 6bc3291573c02..e665baf7454a5 100644
--- a/net/dsa/port.h
+++ b/net/dsa/port.h
@@ -46,22 +46,24 @@ int dsa_port_mst_enable(struct dsa_port *dp, bool on,
 int dsa_port_vlan_msti(struct dsa_port *dp,
 		       const struct switchdev_vlan_msti *msti);
 int dsa_port_mtu_change(struct dsa_port *dp, int new_mtu);
-int dsa_port_fdb_add(struct dsa_port *dp, const unsigned char *addr,
-		     u16 vid);
-int dsa_port_fdb_del(struct dsa_port *dp, const unsigned char *addr,
-		     u16 vid);
+int dsa_port_fdb_add(struct dsa_port *dp, const struct dsa_bridge *bridge,
+		     const unsigned char *addr, u16 vid);
+int dsa_port_fdb_del(struct dsa_port *dp, const struct dsa_bridge *bridge,
+		     const unsigned char *addr, u16 vid);
 int dsa_port_standalone_host_fdb_add(struct dsa_port *dp,
 				     const unsigned char *addr, u16 vid);
 int dsa_port_standalone_host_fdb_del(struct dsa_port *dp,
 				     const unsigned char *addr, u16 vid);
-int dsa_port_bridge_host_fdb_add(struct dsa_port *dp, const unsigned char *addr,
-				 u16 vid);
-int dsa_port_bridge_host_fdb_del(struct dsa_port *dp, const unsigned char *addr,
-				 u16 vid);
-int dsa_port_lag_fdb_add(struct dsa_port *dp, const unsigned char *addr,
-			 u16 vid);
-int dsa_port_lag_fdb_del(struct dsa_port *dp, const unsigned char *addr,
-			 u16 vid);
+int dsa_port_bridge_host_fdb_add(struct dsa_port *dp,
+				 const struct dsa_bridge *bridge,
+				 const unsigned char *addr, u16 vid);
+int dsa_port_bridge_host_fdb_del(struct dsa_port *dp,
+				 const struct dsa_bridge *bridge,
+				 const unsigned char *addr, u16 vid);
+int dsa_port_lag_fdb_add(struct dsa_port *dp, const struct dsa_bridge *bridge,
+			 const unsigned char *addr, u16 vid);
+int dsa_port_lag_fdb_del(struct dsa_port *dp, const struct dsa_bridge *bridge,
+			 const unsigned char *addr, u16 vid);
 int dsa_port_fdb_dump(struct dsa_port *dp, dsa_fdb_dump_cb_t *cb, void *data);
 int dsa_port_mdb_add(const struct dsa_port *dp,
 		     const struct switchdev_obj_port_mdb *mdb);
diff --git a/net/dsa/user.c b/net/dsa/user.c
index 041f9060c8ef0..24f9914296660 100644
--- a/net/dsa/user.c
+++ b/net/dsa/user.c
@@ -43,6 +43,7 @@ struct dsa_switchdev_event_work {
 	unsigned char addr[ETH_ALEN];
 	u16 vid;
 	bool host_addr;
+	bool replay;
 };
 
 enum dsa_standalone_event {
@@ -2813,6 +2814,7 @@ int dsa_user_create(struct dsa_port *port)
 
 	p->dp = port;
 	INIT_LIST_HEAD(&p->mall_tc_list);
+	INIT_LIST_HEAD(&p->bridge_host_fdbs);
 	port->user = user_dev;
 	dsa_user_setup_tagger(user_dev);
 
@@ -3627,6 +3629,22 @@ static int dsa_user_netdevice_event(struct notifier_block *nb,
 	return NOTIFY_DONE;
 }
 
+static bool dsa_foreign_dev_check(const struct net_device *dev,
+				  const struct net_device *foreign_dev)
+{
+	const struct dsa_port *dp = dsa_user_to_port(dev);
+	struct dsa_switch_tree *dst = dp->ds->dst;
+
+	if (netif_is_bridge_master(foreign_dev))
+		return !dsa_tree_offloads_bridge_dev(dst, foreign_dev);
+
+	if (netif_is_bridge_port(foreign_dev))
+		return !dsa_tree_offloads_bridge_port(dst, foreign_dev);
+
+	/* Everything else is foreign */
+	return true;
+}
+
 static void
 dsa_fdb_offload_notify(struct dsa_switchdev_event_work *switchdev_work)
 {
@@ -3639,6 +3657,270 @@ dsa_fdb_offload_notify(struct dsa_switchdev_event_work *switchdev_work)
 				 switchdev_work->orig_dev, &info.info, NULL);
 }
 
+/* The bridge notifies a roaming entry without a lock, so with two CPUs the
+ * deletion from the old port and the addition on the new one can arrive
+ * twice or out of order. Hold the host address while the bridge has the
+ * entry on a foreign port or as a local entry. A deletion that still finds
+ * the entry on its own port was sent before the bridge changed the port. An
+ * entry on a user port, also one of another tree, is notified only to that
+ * port.
+ */
+enum dsa_host_fdb_want {
+	DSA_HOST_FDB_NONE,
+	DSA_HOST_FDB_FOREIGN,
+	DSA_HOST_FDB_LOCAL,
+};
+
+static enum dsa_host_fdb_want
+dsa_user_bridge_host_fdb_wanted(struct net_device *dev,
+				const struct dsa_bridge *bridge,
+				const unsigned char *addr, u16 vid,
+				const struct net_device *leaving)
+{
+	struct dsa_port *dp = dsa_user_to_port(dev);
+	enum dsa_host_fdb_want want = DSA_HOST_FDB_NONE;
+	struct net_device *brport;
+	bool by_user = false;
+
+	rcu_read_lock();
+	brport = br_switchdev_fdb_find_port(bridge->dev, addr, vid, &by_user);
+	if (brport == bridge->dev)
+		want = DSA_HOST_FDB_LOCAL;
+	/* Learned entries on foreign ports only count with assisted learning,
+	 * as in dsa_user_fdb_event()
+	 */
+	else if (brport && brport != leaving && !dsa_user_dev_check(brport) &&
+		 dsa_foreign_dev_check(dev, brport) &&
+		 (by_user || dp->ds->assisted_learning_on_cpu_port))
+		want = DSA_HOST_FDB_FOREIGN;
+	rcu_read_unlock();
+
+	return want;
+}
+
+/* Hold, release or retag the host address; return whether it is held
+ * afterwards
+ */
+static bool dsa_user_bridge_host_fdb_set(struct net_device *dev,
+					 const struct dsa_bridge *bridge,
+					 const unsigned char *addr, u16 vid,
+					 enum dsa_host_fdb_want want)
+{
+	struct dsa_user_priv *p = netdev_priv(dev);
+	struct dsa_port *dp = p->dp;
+	struct dsa_user_host_fdb *a, *held = NULL;
+	bool local = want == DSA_HOST_FDB_LOCAL;
+	int err;
+
+	list_for_each_entry(a, &p->bridge_host_fdbs, list) {
+		if (ether_addr_equal(a->addr, addr) && a->vid == vid) {
+			held = a;
+			break;
+		}
+	}
+
+	if (want != DSA_HOST_FDB_NONE && held) {
+		if (held->local != local) {
+			held->local = local;
+			atomic_add(local ? -1 : 1,
+				   &p->bridge_host_fdbs_foreign_n);
+		}
+		return true;
+	}
+
+	if (want == DSA_HOST_FDB_NONE && !held)
+		return false;
+
+	if (want != DSA_HOST_FDB_NONE) {
+		a = kzalloc_obj(*a);
+		if (!a)
+			return false;
+
+		err = dsa_port_bridge_host_fdb_add(dp, bridge, addr, vid);
+		if (err) {
+			dev_err(dp->ds->dev,
+				"port %d failed to add %pM vid %d to fdb: %d\n",
+				dp->index, addr, vid, err);
+			kfree(a);
+			return false;
+		}
+
+		ether_addr_copy(a->addr, addr);
+		a->vid = vid;
+		a->local = local;
+		list_add(&a->list, &p->bridge_host_fdbs);
+		atomic_inc(&p->bridge_host_fdbs_n);
+		if (!local)
+			atomic_inc(&p->bridge_host_fdbs_foreign_n);
+		return true;
+	}
+
+	/* The conduit's unicast filter is released before the switch entry, so
+	 * the record goes even when the latter fails.
+	 */
+	err = dsa_port_bridge_host_fdb_del(dp, bridge, addr, vid);
+	if (err)
+		dev_err(dp->ds->dev,
+			"port %d failed to delete %pM vid %d from fdb: %d\n",
+			dp->index, addr, vid, err);
+
+	list_del(&held->list);
+	atomic_dec(&p->bridge_host_fdbs_n);
+	if (!held->local)
+		atomic_dec(&p->bridge_host_fdbs_foreign_n);
+	kfree(held);
+
+	return false;
+}
+
+static void
+dsa_user_bridge_host_fdb_sync(struct dsa_switchdev_event_work *switchdev_work)
+{
+	bool del = switchdev_work->event == SWITCHDEV_FDB_DEL_TO_DEVICE;
+	const struct net_device *leaving =
+		del ? switchdev_work->orig_dev : NULL;
+	struct net_device *dev = switchdev_work->dev;
+	const unsigned char *addr = switchdev_work->addr;
+	u16 vid = switchdev_work->vid;
+	enum dsa_host_fdb_want want = DSA_HOST_FDB_NONE;
+	const struct dsa_bridge *bridge;
+	bool held;
+
+	/* Cleared before the workqueue is flushed and the bridge is freed */
+	bridge = READ_ONCE(dsa_user_to_port(dev)->bridge);
+	if (!bridge)
+		return;
+
+	if (!(del && switchdev_work->replay))
+		want = dsa_user_bridge_host_fdb_wanted(dev, bridge, addr, vid,
+						       leaving);
+
+	held = dsa_user_bridge_host_fdb_set(dev, bridge, addr, vid, want);
+
+	if (held) {
+		/* Pairs with the barrier in dsa_user_bridge_host_fdb_release():
+		 * either it sees the record, or the entry has moved by the time
+		 * it is looked up again.
+		 */
+		smp_mb();
+		want = dsa_user_bridge_host_fdb_wanted(dev, bridge, addr, vid,
+						       leaving);
+		held = dsa_user_bridge_host_fdb_set(dev, bridge, addr, vid,
+						    want);
+	}
+
+	if (held && !del)
+		dsa_fdb_offload_notify(switchdev_work);
+}
+
+/* Only the user port an entry is on gets its events, so when the entry roams
+ * there, release it on the ports of the bridge that still hold it.
+ */
+static void dsa_user_bridge_host_fdb_release_work(struct work_struct *work)
+{
+	struct dsa_switchdev_event_work *switchdev_work =
+		container_of(work, struct dsa_switchdev_event_work, work);
+	const unsigned char *addr = switchdev_work->addr;
+	struct net_device *dev = switchdev_work->dev;
+	u16 vid = switchdev_work->vid;
+	const struct dsa_bridge *bridge;
+	enum dsa_host_fdb_want want;
+
+	bridge = READ_ONCE(dsa_user_to_port(dev)->bridge);
+	if (bridge) {
+		want = dsa_user_bridge_host_fdb_wanted(dev, bridge, addr, vid,
+						       NULL);
+		dsa_user_bridge_host_fdb_set(dev, bridge, addr, vid, want);
+	}
+
+	kfree(switchdev_work);
+}
+
+struct dsa_user_release_ctx {
+	const unsigned char *addr;
+	u16 vid;
+	bool dynamic;
+	int err;
+};
+
+static int dsa_user_bridge_host_fdb_release_one(struct net_device *lower,
+						struct netdev_nested_priv *priv)
+{
+	struct dsa_user_release_ctx *ctx = (void *)priv->data;
+	struct dsa_switchdev_event_work *switchdev_work;
+	struct dsa_user_priv *p;
+
+	if (!dsa_user_dev_check(lower))
+		return 0;
+
+	/* A learned entry cannot replace a local one, so only the addresses
+	 * held for foreign entries can be affected
+	 */
+	p = netdev_priv(lower);
+	if (!READ_ONCE(p->dp->bridge) ||
+	    !atomic_read(ctx->dynamic ? &p->bridge_host_fdbs_foreign_n :
+					&p->bridge_host_fdbs_n))
+		return 0;
+
+	switchdev_work = kzalloc_obj(*switchdev_work, GFP_ATOMIC);
+	if (!switchdev_work) {
+		ctx->err = -ENOMEM;
+		return 0;
+	}
+
+	INIT_WORK(&switchdev_work->work, dsa_user_bridge_host_fdb_release_work);
+	switchdev_work->dev = lower;
+	ether_addr_copy(switchdev_work->addr, ctx->addr);
+	switchdev_work->vid = ctx->vid;
+
+	dsa_schedule_work(&switchdev_work->work);
+
+	return 0;
+}
+
+/* Called under rcu_read_lock() */
+static int dsa_user_bridge_host_fdb_release(struct net_device *dev,
+					    const unsigned char *addr, u16 vid,
+					    bool dynamic)
+{
+	struct dsa_user_release_ctx ctx = {
+		.addr = addr, .vid = vid, .dynamic = dynamic,
+	};
+	struct netdev_nested_priv priv = { .data = &ctx };
+	struct net_device *br = dsa_port_bridge_dev_get(dsa_user_to_port(dev));
+
+	if (!br)
+		return 0;
+
+	/* Pairs with the barrier in dsa_user_bridge_host_fdb_sync() */
+	smp_mb();
+	netdev_walk_all_lower_dev_rcu(br, dsa_user_bridge_host_fdb_release_one,
+				      &priv);
+
+	return ctx.err;
+}
+
+void dsa_user_bridge_host_fdb_purge(struct dsa_port *dp,
+				    const struct dsa_bridge *bridge)
+{
+	struct dsa_user_priv *p = netdev_priv(dp->user);
+	struct dsa_user_host_fdb *a, *tmp;
+	int err;
+
+	list_for_each_entry_safe(a, tmp, &p->bridge_host_fdbs, list) {
+		err = dsa_port_bridge_host_fdb_del(dp, bridge, a->addr, a->vid);
+		if (err)
+			dev_err(dp->ds->dev,
+				"port %d failed to delete %pM vid %d from fdb: %d\n",
+				dp->index, a->addr, a->vid, err);
+		list_del(&a->list);
+		atomic_dec(&p->bridge_host_fdbs_n);
+		if (!a->local)
+			atomic_dec(&p->bridge_host_fdbs_foreign_n);
+		kfree(a);
+	}
+}
+
 static void dsa_user_switchdev_event_work(struct work_struct *work)
 {
 	struct dsa_switchdev_event_work *switchdev_work =
@@ -3646,6 +3928,7 @@ static void dsa_user_switchdev_event_work(struct work_struct *work)
 	const unsigned char *addr = switchdev_work->addr;
 	struct net_device *dev = switchdev_work->dev;
 	u16 vid = switchdev_work->vid;
+	const struct dsa_bridge *bridge;
 	struct dsa_switch *ds;
 	struct dsa_port *dp;
 	int err;
@@ -3653,14 +3936,28 @@ static void dsa_user_switchdev_event_work(struct work_struct *work)
 	dp = dsa_user_to_port(dev);
 	ds = dp->ds;
 
+	if (switchdev_work->host_addr) {
+		dsa_user_bridge_host_fdb_sync(switchdev_work);
+		kfree(switchdev_work);
+		return;
+	}
+
+	/* An event that was queued before the port left the bridge finds no
+	 * bridge: dsa_port_bridge_destroy() clears it and flushes the workqueue
+	 * before the bridge is freed.
+	 */
+	bridge = READ_ONCE(dp->bridge);
+	if (!bridge) {
+		kfree(switchdev_work);
+		return;
+	}
+
 	switch (switchdev_work->event) {
 	case SWITCHDEV_FDB_ADD_TO_DEVICE:
-		if (switchdev_work->host_addr)
-			err = dsa_port_bridge_host_fdb_add(dp, addr, vid);
-		else if (dp->lag)
-			err = dsa_port_lag_fdb_add(dp, addr, vid);
+		if (dp->lag)
+			err = dsa_port_lag_fdb_add(dp, bridge, addr, vid);
 		else
-			err = dsa_port_fdb_add(dp, addr, vid);
+			err = dsa_port_fdb_add(dp, bridge, addr, vid);
 		if (err) {
 			dev_err(ds->dev,
 				"port %d failed to add %pM vid %d to fdb: %d\n",
@@ -3671,12 +3968,10 @@ static void dsa_user_switchdev_event_work(struct work_struct *work)
 		break;
 
 	case SWITCHDEV_FDB_DEL_TO_DEVICE:
-		if (switchdev_work->host_addr)
-			err = dsa_port_bridge_host_fdb_del(dp, addr, vid);
-		else if (dp->lag)
-			err = dsa_port_lag_fdb_del(dp, addr, vid);
+		if (dp->lag)
+			err = dsa_port_lag_fdb_del(dp, bridge, addr, vid);
 		else
-			err = dsa_port_fdb_del(dp, addr, vid);
+			err = dsa_port_fdb_del(dp, bridge, addr, vid);
 		if (err) {
 			dev_err(ds->dev,
 				"port %d failed to delete %pM vid %d from fdb: %d\n",
@@ -3689,22 +3984,6 @@ static void dsa_user_switchdev_event_work(struct work_struct *work)
 	kfree(switchdev_work);
 }
 
-static bool dsa_foreign_dev_check(const struct net_device *dev,
-				  const struct net_device *foreign_dev)
-{
-	const struct dsa_port *dp = dsa_user_to_port(dev);
-	struct dsa_switch_tree *dst = dp->ds->dst;
-
-	if (netif_is_bridge_master(foreign_dev))
-		return !dsa_tree_offloads_bridge_dev(dst, foreign_dev);
-
-	if (netif_is_bridge_port(foreign_dev))
-		return !dsa_tree_offloads_bridge_port(dst, foreign_dev);
-
-	/* Everything else is foreign */
-	return true;
-}
-
 static int dsa_user_fdb_event(struct net_device *dev,
 			      struct net_device *orig_dev,
 			      unsigned long event, const void *ctx,
@@ -3718,12 +3997,16 @@ static int dsa_user_fdb_event(struct net_device *dev,
 	if (ctx && ctx != dp)
 		return 0;
 
-	if (!dp->bridge)
+	if (!READ_ONCE(dp->bridge))
 		return 0;
 
 	if (switchdev_fdb_is_dynamically_learned(fdb_info)) {
 		if (dsa_port_offloads_bridge_port(dp, orig_dev))
-			return 0;
+			return event == SWITCHDEV_FDB_ADD_TO_DEVICE ?
+				dsa_user_bridge_host_fdb_release(dev,
+								 fdb_info->addr,
+								 fdb_info->vid,
+								 true) : 0;
 
 		/* FDB entries learned by the software bridge or by foreign
 		 * bridge ports should be installed as host addresses only if
@@ -3739,6 +4022,19 @@ static int dsa_user_fdb_event(struct net_device *dev,
 	if (dsa_foreign_dev_check(dev, orig_dev))
 		host_addr = true;
 
+	/* The entry is on a user port now, so no other port holds it. Release
+	 * the holds before the entry is installed: some switches keep one
+	 * entry per address, which a later release would erase.
+	 */
+	if (!host_addr && event == SWITCHDEV_FDB_ADD_TO_DEVICE) {
+		int err = dsa_user_bridge_host_fdb_release(dev, fdb_info->addr,
+							   fdb_info->vid,
+							   false);
+
+		if (err)
+			return err;
+	}
+
 	/* Check early that we're not doing work in vain.
 	 * Host addresses on LAG ports still require regular FDB ops,
 	 * since the CPU port isn't in a LAG.
@@ -3768,6 +4064,7 @@ static int dsa_user_fdb_event(struct net_device *dev,
 	ether_addr_copy(switchdev_work->addr, fdb_info->addr);
 	switchdev_work->vid = fdb_info->vid;
 	switchdev_work->host_addr = host_addr;
+	switchdev_work->replay = !!ctx;
 
 	dsa_schedule_work(&switchdev_work->work);
 
diff --git a/net/dsa/user.h b/net/dsa/user.h
index 016884bead3cd..ca9bc1d4f50e7 100644
--- a/net/dsa/user.h
+++ b/net/dsa/user.h
@@ -17,6 +17,14 @@ struct netlink_ext_ack;
 extern struct notifier_block dsa_user_switchdev_notifier;
 extern struct notifier_block dsa_user_switchdev_blocking_notifier;
 
+/* A host address held for a bridge FDB entry */
+struct dsa_user_host_fdb {
+	struct list_head list;
+	unsigned char addr[ETH_ALEN];
+	u16 vid;
+	bool local;
+};
+
 struct dsa_user_priv {
 	/* Copy of CPU port xmit for faster access in user transmit hot path */
 	struct sk_buff *	(*xmit)(struct sk_buff *skb,
@@ -33,6 +41,18 @@ struct dsa_user_priv {
 
 	/* TC context */
 	struct list_head	mall_tc_list;
+
+	/* Host addresses of bridge FDB entries held through this port,
+	 * changed from the DSA workqueue, or with the port out of the bridge
+	 * and the workqueue flushed
+	 */
+	struct list_head	bridge_host_fdbs;
+
+	/* Length of bridge_host_fdbs, and how many of its entries are not
+	 * local to the bridge; read without the workqueue
+	 */
+	atomic_t		bridge_host_fdbs_n;
+	atomic_t		bridge_host_fdbs_foreign_n;
 };
 
 void dsa_user_mii_bus_init(struct dsa_switch *ds);
@@ -47,6 +67,8 @@ void dsa_user_host_uc_uninstall(struct net_device *dev);
 void dsa_user_sync_ha(struct net_device *dev);
 void dsa_user_unsync_ha(struct net_device *dev);
 void dsa_user_setup_tagger(struct net_device *user);
+void dsa_user_bridge_host_fdb_purge(struct dsa_port *dp,
+				    const struct dsa_bridge *bridge);
 int dsa_user_change_mtu(struct net_device *dev, int new_mtu);
 int dsa_user_change_conduit(struct net_device *dev, struct net_device *conduit,
 			    struct netlink_ext_ack *extack);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-11-14 22:53 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-14 22:53 [PATCH net-next v2 0/2] net: dsa: fix host fdb refcount when an entry roams Julius Bairaktaris
2026-11-14 22:53 ` [PATCH net-next v2 1/2] net: bridge: switchdev: add br_switchdev_fdb_find_port() Julius Bairaktaris
2026-11-14 22:53 ` [PATCH net-next v2 2/2] net: dsa: fix host fdb refcount when a bridge fdb entry roams Julius Bairaktaris

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