[PATCH net] net: bridge: fdb: hold hash_lock when an entry roams

From: Julius Bairaktaris

Date: Sun Oct 04 2026 - 08:43:48 EST


br_fdb_update() lets an entry roam to a new port without holding
hash_lock. It notifies switchdev that the entry left the old port,
writes the new port, then notifies the addition. When two CPUs receive
the same source address on different ports, these steps interleave: a
driver sees two deletions for one addition, or an addition for the port
the other CPU wrote.

DSA counts references to a host address on the CPU port. The extra
deletion fails and the extra addition is never released:

qca-ppe 3a000000.ppe: port 5 failed to delete 02:5a:0b:a2:1a:46 vid 0 from fdb: -2

With one address roaming between a DSA user port and a Wi-Fi AP port of
the same bridge, the error appears 3-6 times per address when the two
ports receive on different CPUs, and not at all when they share one CPU
(4 runs each). With this change it does not appear (6 runs, different
CPUs).

Take hash_lock when the entry roams or its flags change, and send both
notifications under it. The common case, where the entry neither roams
nor changes, stays lockless.

Fixes: 90dc8fd36078 ("net: bridge: notify switchdev of disappearance of old FDB entry upon migration")
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Julius Bairaktaris <julius@xxxxxxxxxxxxxx>
---

Notes:
net-next 941056f91907 ("net: bridge: fdb: factor out existing entry updates")
moves this code into __fdb_update(); the same change applies there.

net/bridge/br_fdb.c | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/net/bridge/br_fdb.c b/net/bridge/br_fdb.c
index e4570bbed854..c8680ae3ef08 100644
--- a/net/bridge/br_fdb.c
+++ b/net/bridge/br_fdb.c
@@ -995,8 +995,17 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
fdb_modified = __fdb_mark_active(fdb);
}

- /* fastpath: update of existing entry */
- if (unlikely(source != READ_ONCE(fdb->dst) &&
+ if (likely(!fdb_modified &&
+ (source == READ_ONCE(fdb->dst) ||
+ test_bit(BR_FDB_STICKY, &fdb->flags)) &&
+ !test_bit(BR_FDB_ADDED_BY_USER, &flags)))
+ return;
+
+ /* keep a roam and its two switchdev notifications
+ * atomic against a roam on another CPU
+ */
+ spin_lock(&br->hash_lock);
+ if (unlikely(source != fdb->dst &&
!test_bit(BR_FDB_STICKY, &fdb->flags))) {
br_switchdev_fdb_notify(br, fdb, RTM_DELNEIGH);
WRITE_ONCE(fdb->dst, source);
@@ -1023,6 +1032,7 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
trace_br_fdb_update(br, source, addr, vid, flags);
fdb_notify(br, fdb, RTM_NEWNEIGH, true);
}
+ spin_unlock(&br->hash_lock);
}
} else {
spin_lock(&br->hash_lock);
--
2.53.0