From: Stanislaw Pal <kuncy7@gmail.com>
To: Christian Marangi <ansuelsmth@gmail.com>,
Andrew Lunn <andrew@lunn.ch>, Vladimir Oltean <olteanv@gmail.com>
Cc: "David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@kernel.org>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Yongzhao Chen <yongzhao.derek@gmail.com>,
netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH net-next v2 2/4] net: dsa: qca8k: drop MIB autocast frames once the request is over
Date: Wed, 4 Nov 2026 22:07:55 +0100 [thread overview]
Message-ID: <20261104210757.467964-3-kuncy7@gmail.com> (raw)
In-Reply-To: <20261104210757.467964-1-kuncy7@gmail.com>
qca8k_get_ethtool_stats_eth() points mib_eth_data at the caller's
ethtool buffer, asks the switch for an autocast and waits 5 ms for the
frames. On a timeout it returns with the request still published, and
the caller falls back to register reads; ethtool_get_stats() then frees
the buffer. The autocast handler runs from the conduit's receive path
without the request's mutex, so a frame that arrives late still writes
the requested port's counters through mib_eth_data->data into freed
memory, and frames beyond the seven expected ones underflow port_parsed.
ETHTOOL_GSTATS does not need CAP_NET_ADMIN.
Protect the request with a spinlock that the handler takes, retire it
on the way out, and let the handler retire it when the last frame
completes it, so that frames arriving after that are dropped without
touching data.
An autocast frame carries no identity of the request it answers. Once a
request has timed out and the next one is published, the switch may
still be sending the earlier autocast, and its frames count for the
new request. Wait for MIB_BUSY to clear before asking for the next
autocast, as qca8k_mib_init() does after a flush, so that at most the
frames already on their way to the host can be taken for the new
request, and track the ports seen as a bitmap instead of a countdown,
so that a second frame of a port cannot complete the request before
the other ports have reported. The requested port's counters then come
from the newest frame of that port, which is at most one autocast
behind - the same as a register read racing the hardware.
An LLM assistant (Claude, claude-fable-5-1) helped analyse the race
and write the change and this description; I reviewed both. Built with
W=1 on arm64; tested on a QCA8337 behind an IPQ5018
(GL.iNet GL-B3000) with tag_qca, with the request timeout forced to 0
to exercise the late frame path.
Fixes: 5c957c7ca78c ("net: dsa: qca8k: add support for mib autocast in Ethernet packet")
Signed-off-by: Stanislaw Pal <kuncy7@gmail.com>
---
drivers/net/dsa/qca/qca8k-8xxx.c | 47 ++++++++++++++++++++++++++----
drivers/net/dsa/qca/qca8k-common.c | 2 +-
drivers/net/dsa/qca/qca8k.h | 6 ++--
3 files changed, 47 insertions(+), 8 deletions(-)
diff --git a/drivers/net/dsa/qca/qca8k-8xxx.c b/drivers/net/dsa/qca/qca8k-8xxx.c
index 1242ee3469..86b20bbc43 100644
--- a/drivers/net/dsa/qca/qca8k-8xxx.c
+++ b/drivers/net/dsa/qca/qca8k-8xxx.c
@@ -1676,10 +1676,18 @@ static void qca8k_mib_autocast_handler(struct dsa_switch *ds, struct sk_buff *sk
mib_ethhdr = (struct mib_ethhdr *)skb_mac_header(skb);
mib_eth_data = &priv->mib_eth_data;
+ spin_lock(&mib_eth_data->lock);
+
+ /* A late frame of a request that has already returned */
+ if (!mib_eth_data->data)
+ goto unlock;
+
/* The switch autocast every port. Ignore other packet and
* parse only the requested one.
*/
port = FIELD_GET(QCA_HDR_RECV_SOURCE_PORT, ntohs(mib_ethhdr->hdr));
+ if (port >= QCA8K_NUM_PORTS)
+ goto unlock;
if (port != mib_eth_data->req_port)
goto exit;
@@ -1704,9 +1712,17 @@ static void qca8k_mib_autocast_handler(struct dsa_switch *ds, struct sk_buff *sk
}
exit:
- /* Complete on receiving all the mib packet */
- if (refcount_dec_and_test(&mib_eth_data->port_parsed))
+ /* Complete once a frame of every port has arrived. A second frame
+ * of a port, left over from an earlier autocast, cannot end the
+ * request early.
+ */
+ mib_eth_data->ports_seen |= BIT(port);
+ if (mib_eth_data->ports_seen == GENMASK(QCA8K_NUM_PORTS - 1, 0)) {
+ mib_eth_data->data = NULL;
complete(&mib_eth_data->rw_done);
+ }
+unlock:
+ spin_unlock(&mib_eth_data->lock);
}
static int
@@ -1721,13 +1737,25 @@ qca8k_get_ethtool_stats_eth(struct dsa_switch *ds, int port, u64 *data)
mutex_lock(&mib_eth_data->mutex);
+ mutex_lock(&priv->reg_mutex);
+
+ /* A request that timed out may have left the switch still sending
+ * its autocast. Let it finish before asking for another one, so
+ * that as few of its frames as possible are taken for ours.
+ */
+ ret = qca8k_busy_wait(priv, QCA8K_REG_MIB, QCA8K_MIB_BUSY);
+ if (ret) {
+ mutex_unlock(&priv->reg_mutex);
+ goto out;
+ }
+
reinit_completion(&mib_eth_data->rw_done);
+ spin_lock_bh(&mib_eth_data->lock);
mib_eth_data->req_port = dp->index;
mib_eth_data->data = data;
- refcount_set(&mib_eth_data->port_parsed, QCA8K_NUM_PORTS);
-
- mutex_lock(&priv->reg_mutex);
+ mib_eth_data->ports_seen = 0;
+ spin_unlock_bh(&mib_eth_data->lock);
/* Send mib autocast request */
ret = regmap_update_bits(priv->regmap, QCA8K_REG_MIB,
@@ -1743,6 +1771,14 @@ qca8k_get_ethtool_stats_eth(struct dsa_switch *ds, int port, u64 *data)
ret = wait_for_completion_timeout(&mib_eth_data->rw_done, QCA8K_ETHERNET_TIMEOUT);
exit:
+ /* On a timeout the frames can still arrive after the caller has
+ * freed data: retire the request so that the handler drops them.
+ */
+ spin_lock_bh(&mib_eth_data->lock);
+ mib_eth_data->data = NULL;
+ spin_unlock_bh(&mib_eth_data->lock);
+
+out:
mutex_unlock(&mib_eth_data->mutex);
return ret;
@@ -2120,6 +2156,7 @@ qca8k_sw_probe(struct mdio_device *mdiodev)
init_completion(&priv->mgmt_eth_data.rw_done);
mutex_init(&priv->mib_eth_data.mutex);
+ spin_lock_init(&priv->mib_eth_data.lock);
init_completion(&priv->mib_eth_data.rw_done);
priv->ds->dev = &mdiodev->dev;
diff --git a/drivers/net/dsa/qca/qca8k-common.c b/drivers/net/dsa/qca/qca8k-common.c
index ab978cc836..f1e7e86f47 100644
--- a/drivers/net/dsa/qca/qca8k-common.c
+++ b/drivers/net/dsa/qca/qca8k-common.c
@@ -101,7 +101,7 @@ const struct regmap_access_table qca8k_readable_table = {
.n_yes_ranges = ARRAY_SIZE(qca8k_readable_ranges),
};
-static int qca8k_busy_wait(struct qca8k_priv *priv, u32 reg, u32 mask)
+int qca8k_busy_wait(struct qca8k_priv *priv, u32 reg, u32 mask)
{
u32 val;
diff --git a/drivers/net/dsa/qca/qca8k.h b/drivers/net/dsa/qca/qca8k.h
index 9563388930..79754755f1 100644
--- a/drivers/net/dsa/qca/qca8k.h
+++ b/drivers/net/dsa/qca/qca8k.h
@@ -402,9 +402,10 @@ struct qca8k_mgmt_eth_data {
struct qca8k_mib_eth_data {
struct completion rw_done;
struct mutex mutex; /* Process one command at time */
- refcount_t port_parsed; /* Counter to track parsed port */
+ spinlock_t lock; /* Protects the request against the rx handler */
+ unsigned long ports_seen; /* Ports whose frame arrived, under lock */
u8 req_port;
- u64 *data; /* pointer to ethtool data */
+ u64 *data; /* pointer to ethtool data, NULL with no request pending */
};
struct qca8k_ports_config {
@@ -501,6 +502,7 @@ static inline u32 qca8k_port_to_phy(int port)
/* Common setup function */
extern const struct qca8k_mib_desc ar8327_mib[];
extern const struct regmap_access_table qca8k_readable_table;
+int qca8k_busy_wait(struct qca8k_priv *priv, u32 reg, u32 mask);
int qca8k_mib_init(struct qca8k_priv *priv);
void qca8k_port_set_status(struct qca8k_priv *priv, int port, int enable);
int qca8k_read_switch_id(struct qca8k_priv *priv);
--
2.43.0
next prev parent reply other threads:[~2026-11-04 21:08 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-04 21:07 [PATCH net-next v2 0/4] net: dsa: qca8k: fix the MIB autocast and management frame handlers Stanislaw Pal
2026-11-04 21:07 ` [PATCH net-next v2 1/4] net: dsa: qca8k: read each register into its own word over MDIO Stanislaw Pal
2026-11-04 21:07 ` Stanislaw Pal [this message]
2026-11-04 21:07 ` [PATCH net-next v2 3/4] net: dsa: qca8k: check the length of MIB autocast frames Stanislaw Pal
2026-11-04 21:07 ` [PATCH net-next v2 4/4] net: dsa: qca8k: bound the data of a management ack frame Stanislaw Pal
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261104210757.467964-3-kuncy7@gmail.com \
--to=kuncy7@gmail.com \
--cc=andrew@lunn.ch \
--cc=ansuelsmth@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=olteanv@gmail.com \
--cc=pabeni@redhat.com \
--cc=yongzhao.derek@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®