net: bridge: use a stable FDB dst snapshot in RCU readers
Subsystems (3)
A message that is not in the archive (sent off-list or not yet received): [email protected]
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.
--- 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
@@ -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
@@ -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 &ðer_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 &ðer_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
Quoted 13 lines · Ren Wei
From: Zhengchuan Liang <[email protected]>
Local FDB entries can be rewritten in place by
fdb_delete_local(), which updatesf->dstto another port or toNULLwhile keeping the entry alive. Several bridge RCU readers inspectf->dst, includingbr_fdb_fillbuf()through thebrforward_read()sysfs path.These readers currently load
f->dstmultiple times and can therefore observe inconsistent values across the check and later dereference. Inbr_fdb_fillbuf(), this means a concurrent local-FDB update can changef->dstafter the NULL check and before theport_nodereference, leading to a NULL-ptr-deref.
Quoted 4 lines
Fix this by taking a single
READ_ONCE()snapshot off->dstin each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-placef->dstupdates infdb_delete_local()withWRITE_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")Reported-by Yifan Wu <[email protected]>Reported-by Juefei Pu <[email protected]>Co-developed-by Yuan Tan <[email protected]>Signed-off-by Yuan Tan <[email protected]>Suggested-by Xin Liu <[email protected]>Tested-by Ren Wei <[email protected]>Signed-off-by Zhengchuan Liang <[email protected]>Signed-off-by Ren Wei <[email protected]>
[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? "
Paolo Abeni
maintainerQuoted 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 updatesf->dstto another port or toNULLwhile keeping the entry alive. Several bridge RCU readers inspectf->dst, includingbr_fdb_fillbuf()through thebrforward_read()sysfs path.These readers currently load
f->dstmultiple times and can therefore observe inconsistent values across the check and later dereference. Inbr_fdb_fillbuf(), this means a concurrent local-FDB update can changef->dstafter the NULL check and before theport_nodereference, leading to a NULL-ptr-deref.Quoted 4 lines
Fix this by taking a single
READ_ONCE()snapshot off->dstin each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-placef->dstupdates infdb_delete_local()withWRITE_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 commit3e19ae7c6fd6("net: bridge: useREAD_ONCE()andWRITE_ONCE()compiler barriers forfdb->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
Quoted 28 lines · Ren Wei
From: Zhengchuan Liang <[email protected]>
Local FDB entries can be rewritten in place by
fdb_delete_local(), which updatesf->dstto another port or toNULLwhile keeping the entry alive. Several bridge RCU readers inspectf->dst, includingbr_fdb_fillbuf()through thebrforward_read()sysfs path.These readers currently load
f->dstmultiple times and can therefore observe inconsistent values across the check and later dereference. Inbr_fdb_fillbuf(), this means a concurrent local-FDB update can changef->dstafter the NULL check and before theport_nodereference, leading to a NULL-ptr-deref.Fix this by taking a single
READ_ONCE()snapshot off->dstin each affected RCU reader and using that snapshot for the rest of the access sequence. Also publish the in-placef->dstupdates infdb_delete_local()withWRITE_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")Reported-by Yifan Wu <[email protected]>Reported-by Juefei Pu <[email protected]>Co-developed-by Yuan Tan <[email protected]>Signed-off-by Yuan Tan <[email protected]>Suggested-by Xin Liu <[email protected]>Tested-by Ren Wei <[email protected]>Signed-off-by Zhengchuan Liang <[email protected]>Signed-off-by Ren Wei <[email protected]>
Quoted 5 lines
--- net/bridge/br_arp_nd_proxy.c | 8 +++++--- net/bridge/br_fdb.c | 28 ++++++++++++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-)
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 updatesf->dstto another port or toNULLwhile keeping the entry alive. Several bridge RCU readers inspectf->dst, includingbr_fdb_fillbuf()through thebrforward_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