Skip to contentSkip to conversation

net: bridge: use a stable FDB dst snapshot in RCU readers

PATCH Applied Ren Wei 5 participants 5 messages Apr 13 – Apr 16 1 message not in the archive

Subsystems (3)
lore
Share & tools
lore

A message that is not in the archive (sent off-list or not yet received): [email protected]

Ren Wei

author
#1 lore raw

PATCH 1/1 net: bridge: use a stable FDB dst snapshot in RCU readers

From: Zhengchuan Liang <[email protected]>

Local FDB entries can be rewritten in place by fdb_delete_local(), which updates f->dst to another port or to NULL while keeping the entry alive. Several bridge RCU readers inspect f->dst, including br_fdb_fillbuf() through the brforward_read() sysfs path.

These readers currently load f->dst multiple times and can therefore observe inconsistent values across the check and later dereference. In br_fdb_fillbuf(), this means a concurrent local-FDB update can change f->dst after the NULL check and before the port_no dereference, leading to a NULL-ptr-deref.

Fix this by taking a single READ_ONCE() snapshot of f->dst in each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-place f->dst updates in fdb_delete_local() with WRITE_ONCE() so the readers and writer use matching access patterns.

Fixes 960b589f86c7 ("bridge: Properly check if local fdb entry can be deleted in br_fdb_change_mac_address")
---
 net/bridge/br_arp_nd_proxy.c |  8 +++++---
 net/bridge/br_fdb.c          | 28 ++++++++++++++++++----------
 2 files changed, 23 insertions(+), 13 deletions(-)
net/bridge/br_arp_nd_proxy.c +5 -3
index 6b5595868a39c..7ace0f4941bb6 100644
@@ -202,11 +202,12 @@ void br_do_proxy_suppress_arp(struct sk_buff *skb, struct net_bridge *br,
 
 		f = br_fdb_find_rcu(br, n->ha, vid);
 		if (f) {
+			const struct net_bridge_port *dst = READ_ONCE(f->dst);
 			bool replied = false;
 
 			if ((p && (p->flags & BR_PROXYARP)) ||
-			    (f->dst && (f->dst->flags & BR_PROXYARP_WIFI)) ||
-			    br_is_neigh_suppress_enabled(f->dst, vid)) {
+			    (dst && (dst->flags & BR_PROXYARP_WIFI)) ||
+			    br_is_neigh_suppress_enabled(dst, vid)) {
 				if (!vid)
 					br_arp_send(br, p, skb->dev, sip, tip,
 						    sha, n->ha, sha, 0, 0);
@@ -470,9 +471,10 @@ void br_do_suppress_nd(struct sk_buff *skb, struct net_bridge *br,
 
 		f = br_fdb_find_rcu(br, n->ha, vid);
 		if (f) {
+			const struct net_bridge_port *dst = READ_ONCE(f->dst);
 			bool replied = false;
 
-			if (br_is_neigh_suppress_enabled(f->dst, vid)) {
+			if (br_is_neigh_suppress_enabled(dst, vid)) {
 				if (vid != 0)
 					br_nd_send(br, p, skb, n,
 						   skb->vlan_proto,
net/bridge/br_fdb.c +18 -10
index e2c17f620f009..6eb3ab69a5140 100644
@@ -236,6 +236,7 @@ struct net_device *br_fdb_find_port(const struct net_device *br_dev,
 				    const unsigned char *addr,
 				    __u16 vid)
 {
+	const struct net_bridge_port *dst;
 	struct net_bridge_fdb_entry *f;
 	struct net_device *dev = NULL;
 	struct net_bridge *br;
@@ -248,8 +249,11 @@ struct net_device *br_fdb_find_port(const struct net_device *br_dev,
 	br = netdev_priv(br_dev);
 	rcu_read_lock();
 	f = br_fdb_find_rcu(br, addr, vid);
-	if (f && f->dst)
-		dev = f->dst->dev;
+	if (f) {
+		dst = READ_ONCE(f->dst);
+		if (dst)
+			dev = dst->dev;
+	}
 	rcu_read_unlock();
 
 	return dev;
@@ -346,7 +350,7 @@ static void fdb_delete_local(struct net_bridge *br,
 		vg = nbp_vlan_group(op);
 		if (op != p && ether_addr_equal(op->dev->dev_addr, addr) &&
 		    (!vid || br_vlan_find(vg, vid))) {
-			f->dst = op;
+			WRITE_ONCE(f->dst, op);
 			clear_bit(BR_FDB_ADDED_BY_USER, &f->flags);
 			return;
 		}
@@ -357,7 +361,7 @@ static void fdb_delete_local(struct net_bridge *br,
 	/* Maybe bridge device has same hw addr? */
 	if (p && ether_addr_equal(br->dev->dev_addr, addr) &&
 	    (!vid || (v && br_vlan_should_use(v)))) {
-		f->dst = NULL;
+		WRITE_ONCE(f->dst, NULL);
 		clear_bit(BR_FDB_ADDED_BY_USER, &f->flags);
 		return;
 	}
@@ -928,6 +932,7 @@ int br_fdb_test_addr(struct net_device *dev, unsigned char *addr)
 int br_fdb_fillbuf(struct net_bridge *br, void *buf,
 		   unsigned long maxnum, unsigned long skip)
 {
+	const struct net_bridge_port *dst;
 	struct net_bridge_fdb_entry *f;
 	struct __fdb_entry *fe = buf;
 	unsigned long delta;
@@ -944,7 +949,8 @@ int br_fdb_fillbuf(struct net_bridge *br, void *buf,
 			continue;
 
 		/* ignore pseudo entry for local MAC address */
-		if (!f->dst)
+		dst = READ_ONCE(f->dst);
+		if (!dst)
 			continue;
 
 		if (skip) {
@@ -956,8 +962,8 @@ int br_fdb_fillbuf(struct net_bridge *br, void *buf,
 		memcpy(fe->mac_addr, f->key.addr.addr, ETH_ALEN);
 
 		/* due to ABI compat need to split into hi/lo */
-		fe->port_no = f->dst->port_no;
-		fe->port_hi = f->dst->port_no >> 8;
+		fe->port_no = dst->port_no;
+		fe->port_hi = dst->port_no >> 8;
 
 		fe->is_local = test_bit(BR_FDB_LOCAL, &f->flags);
 		if (!test_bit(BR_FDB_STATIC, &f->flags)) {
@@ -1083,9 +1089,11 @@ int br_fdb_dump(struct sk_buff *skb,
 
 	rcu_read_lock();
 	hlist_for_each_entry_rcu(f, &br->fdb_list, fdb_node) {
+		const struct net_bridge_port *dst = READ_ONCE(f->dst);
+
 		if (*idx < ctx->fdb_idx)
 			goto skip;
-		if (filter_dev && (!f->dst || f->dst->dev != filter_dev)) {
+		if (filter_dev && (!dst || dst->dev != filter_dev)) {
 			if (filter_dev != dev)
 				goto skip;
 			/* !f->dst is a special case for bridge
@@ -1093,10 +1101,10 @@ int br_fdb_dump(struct sk_buff *skb,
 			 * Therefore need a little more filtering
 			 * we only want to dump the !f->dst case
 			 */
-			if (f->dst)
+			if (dst)
 				goto skip;
 		}
-		if (!filter_dev && f->dst)
+		if (!filter_dev && dst)
 			goto skip;
 
 		err = fdb_fill_info(skb, br, f,
Signature · 1 line
2.43.0
maintainer Reviewed-by on v1 1/1
#2 lore raw
Quoted 13 lines · Ren Wei

From: Zhengchuan Liang <[email protected]>

Local FDB entries can be rewritten in place by fdb_delete_local(), which updates f->dst to another port or to NULL while keeping the entry alive. Several bridge RCU readers inspect f->dst, including br_fdb_fillbuf() through the brforward_read() sysfs path.

These readers currently load f->dst multiple times and can therefore observe inconsistent values across the check and later dereference. In br_fdb_fillbuf(), this means a concurrent local-FDB update can change f->dst after the NULL check and before the port_no dereference, leading to a NULL-ptr-deref.

Quoted 4 lines

Fix this by taking a single READ_ONCE() snapshot of f->dst in each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-place f->dst updates in fdb_delete_local() with WRITE_ONCE() so the readers and writer use matching access patterns.

Sashiko is complaining [1] about missing READ_ONCE() annotations in some places, but I can handle them in net-next in a similar fashion to commit 3e19ae7c6fd6 ("net: bridge: use READ_ONCE() and WRITE_ONCE() compiler barriers for fdb->dst").

It's also complaining [2] about a not very interesting possible bug in br_fdb_dump() which is pre-existing.

Quoted 11 lines
Fixes 960b589f86c7 ("bridge: Properly check if local fdb entry can be deleted in br_fdb_change_mac_address")

[1]
"
Are there other RCU readers that still need this protection?

For instance, in br_dev_xmit(), br_fdb_find_rcu() returns a local FDB entry which is then passed to br_forward(). If a concurrent fdb_delete_local() sets the entry's dst to NULL, could this cause a NULL pointer dereference if br_forward() is inlined and the compiler emits multiple loads?

Similarly, br_handle_frame_finish() appears to perform an unmarked read of dst->dst, which might race with br_fdb_update().

Also, in br_fdb_delete_by_port(), f->dst is read directly without READ_ONCE(). While called under br->hash_lock, the br_fdb_update() fast path updates f->dst locklessly. Could this trigger KCSAN warnings due to an unmarked data race?
"

[2]
"
Does passing f to fdb_fill_info() allow a concurrent update to change the destination port after the filtering check?

fdb_fill_info() executes a new READ_ONCE(fdb->dst). If f->dst changes between the filter_dev check above and the call to fdb_fill_info(), the dumped entry might claim to be on a device that doesn't match the requested filter_dev.

Should fdb_fill_info() be updated to accept the dst snapshot instead? "

maintainer
#3 lore raw
Quoted 19 lines · Ido Schimmel
Quoted 13 lines · Ren Wei

From: Zhengchuan Liang <[email protected]>

Local FDB entries can be rewritten in place by fdb_delete_local(), which updates f->dst to another port or to NULL while keeping the entry alive. Several bridge RCU readers inspect f->dst, including br_fdb_fillbuf() through the brforward_read() sysfs path.

These readers currently load f->dst multiple times and can therefore observe inconsistent values across the check and later dereference. In br_fdb_fillbuf(), this means a concurrent local-FDB update can change f->dst after the NULL check and before the port_no dereference, leading to a NULL-ptr-deref.

Quoted 4 lines

Fix this by taking a single READ_ONCE() snapshot of f->dst in each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-place f->dst updates in fdb_delete_local() with WRITE_ONCE() so the readers and writer use matching access patterns.

Quoted 4 lines

Sashiko is complaining [1] about missing READ_ONCE() annotations in some places, but I can handle them in net-next in a similar fashion to commit 3e19ae7c6fd6 ("net: bridge: use READ_ONCE() and WRITE_ONCE() compiler barriers for fdb->dst").

I agree they can be handled separately, because they don't look harmful. I think a 'net' patch could be used for such follow-up (data race)

/P

maintainer Acked-by on v1 1/1
#4 lore raw
In reply to Ren Wei
Quoted 28 lines · Ren Wei

From: Zhengchuan Liang <[email protected]>

Local FDB entries can be rewritten in place by fdb_delete_local(), which updates f->dst to another port or to NULL while keeping the entry alive. Several bridge RCU readers inspect f->dst, including br_fdb_fillbuf() through the brforward_read() sysfs path.

These readers currently load f->dst multiple times and can therefore observe inconsistent values across the check and later dereference. In br_fdb_fillbuf(), this means a concurrent local-FDB update can change f->dst after the NULL check and before the port_no dereference, leading to a NULL-ptr-deref.

Fix this by taking a single READ_ONCE() snapshot of f->dst in each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-place f->dst updates in fdb_delete_local() with WRITE_ONCE() so the readers and writer use matching access patterns.

Fixes 960b589f86c7 ("bridge: Properly check if local fdb entry can be deleted in br_fdb_change_mac_address")
Quoted 5 lines
---
  net/bridge/br_arp_nd_proxy.c |  8 +++++---
  net/bridge/br_fdb.c          | 28 ++++++++++++++++++----------
  2 files changed, 23 insertions(+), 13 deletions(-)
#5 lore raw
In reply to Ren Wei
This patch was applied to netdev/net.git (main) by Paolo Abeni <[email protected]>:

Hello:

This patch was applied to netdev/net.git (main)
by Paolo Abeni <[email protected]>:

Quoted 8 lines · On Mon, 13 Apr 2026 17:08:46 +0800 you wrote:

From: Zhengchuan Liang <[email protected]>

Local FDB entries can be rewritten in place by fdb_delete_local(), which updates f->dst to another port or to NULL while keeping the entry alive. Several bridge RCU readers inspect f->dst, including br_fdb_fillbuf() through the brforward_read() sysfs path.

[...]

Here is the summary with links:
- [net,1/1] net: bridge: use a stable FDB dst snapshot in RCU readers

    https://git.kernel.org/netdev/net/c/df4601653201

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html