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")
Cc: [email protected]
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]>
---
 net/bridge/br_arp_nd_proxy.c |  8 +++++---
 net/bridge/br_fdb.c          | 28 ++++++++++++++++++----------
 2 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/net/bridge/br_arp_nd_proxy.c b/net/bridge/br_arp_nd_proxy.c
index 6b5595868a39c..7ace0f4941bb6 100644
--- a/net/bridge/br_arp_nd_proxy.c
+++ b/net/bridge/br_arp_nd_proxy.c
@@ -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,
diff --git a/net/bridge/br_fdb.c b/net/bridge/br_fdb.c
index e2c17f620f009..6eb3ab69a5140 100644
--- a/net/bridge/br_fdb.c
+++ b/net/bridge/br_fdb.c
@@ -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,
-- 
2.43.0


Reply via email to