* [PATCH v10 net-next 0/2] octeontx2-pf: mqprio bandwidth offload for NIX TX schedulers
@ 2026-08-31 13:10 Ratheesh Kannoth
2026-08-31 13:10 ` [PATCH v10 net-next 1/2] net/sched: mqprio: pass qdisc handle to offload drivers Ratheesh Kannoth
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:10 UTC (permalink / raw)
To: bpf, linux-kernel, netdev
Cc: andrew+netdev, ast, daniel, davem, edumazet, hawk, horms, jhs,
jiri, john.fastabend, kuba, pabeni, sdf, sgoutham,
Ratheesh Kannoth
This series adds hardware offload for channel-mode mqprio with
TC_MQPRIO_SHAPER_BW_RATE on Marvell octeontx2 PF devices. Each
non-QoS transmit queue is shaped by programming MDQ CIR/PIR on the NIX TX
scheduler. When bandwidth offload is enabled, the driver allocates one
SMQ per queue, parents every MDQ under TL4[0], and applies each traffic
class min/max rate to the queue(s) in that class. The TX scheduler
hierarchy is reprogrammed through ndo_stop()/ndo_open() on every mqprio
add, replace, or delete.
Patch 1 is required for patch 2. The octeontx2 driver tracks committed
mqprio configuration in snapshots keyed by the owning qdisc handle. That
handle is needed to implement two behaviours that patch 2 relies on:
* tc qdisc replace installs the new mqprio and runs its offload setup
before tearing down the replaced instance. Without the handle, the
driver cannot tell a stale disable callback from the replaced qdisc
apart from teardown of the active configuration, and may clear
hardware shapers or netdev TC state while the new mqprio is being
committed.
* When replace fails after partial programming, the driver rolls back
from the previous snapshot. Matching setup and teardown callbacks to
the same qdisc instance requires the handle passed through the core
mqprio offload path.
Patch 1 extends struct tc_mqprio_qopt_offload with that handle and fills
it from mqprio_enable_offload() and mqprio_disable_offload(). Patch 2
consumes it in the octeontx2 PF mqprio offload path.
The driver rejects offload unless the interface is running and the
device supports CIR+PIR shaping. Per-TC rates are rejected when a
traffic class spans more than one queue, and concurrent PFC, XDP, or SDP
rep use is blocked. Ethtool channel count changes are blocked while mqprio
bandwidth offload is active.
Ratheesh Kannoth (2):
net/sched: mqprio: pass qdisc handle to offload drivers
octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
.../ethernet/marvell/octeontx2/af/rvu_nix.c | 6 +-
.../marvell/octeontx2/nic/otx2_common.c | 145 +++-
.../marvell/octeontx2/nic/otx2_common.h | 27 +
.../marvell/octeontx2/nic/otx2_dcbnl.c | 6 +
.../marvell/octeontx2/nic/otx2_ethtool.c | 8 +
.../ethernet/marvell/octeontx2/nic/otx2_pf.c | 13 +
.../ethernet/marvell/octeontx2/nic/otx2_tc.c | 696 ++++++++++++++++++
.../net/ethernet/marvell/octeontx2/nic/qos.c | 3 +
include/net/pkt_sched.h | 1 +
net/sched/sch_mqprio.c | 5 +-
10 files changed, 907 insertions(+), 3 deletions(-)
---
v9 -> v10: Addressed sashiko/jacub comments.
https://sashiko.dev/#/message/20260817032747.1765883-1-rkannoth%40marvell.com
v8 -> v9: Addressed Sashiko comments
https://lore.kernel.org/netdev/aoJ6FhtWue0FHDQV@rkannoth-OptiPlex-7090/
v7 -> v8: Addressed Sashiko comments
https://sashiko.dev/#/patchset/20260811085050.3212280-1-rkannoth%40marvell.com
v6 -> v7: Addressed Sashiko comments
https://sashiko.dev/#/message/20260810034738.1786029-1-rkannoth%40marvell.com
v5 -> v6: Addressed Sashiko comments
https://lore.kernel.org/netdev/20260806095434.1144397-1-rkannoth@marvell.com/
v4 -> v5: Addressed sashiko comments
https://sashiko.dev/#/patchset/20260803042724.3380209-1-rkannoth%40marvell.com
v3 -> v4: Addressed sashiko comments
https://lore.kernel.org/netdev/20260729105139.2302908-1-rkannoth@marvell.com/
v2 -> v3: Addressed sashiko comments
https://lore.kernel.org/netdev/amnYX866mYx02cBe@rkannoth-OptiPlex-7090/T/#m67310cbec48b21c7720858ab3a1ea083a0f8dc10
v1 -> v2: Addressed sashiko comments
https://lore.kernel.org/netdev/20260724075010.2665758-1-rkannoth@marvell.com/
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v10 net-next 1/2] net/sched: mqprio: pass qdisc handle to offload drivers
2026-08-31 13:10 [PATCH v10 net-next 0/2] octeontx2-pf: mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
@ 2026-08-31 13:10 ` Ratheesh Kannoth
2026-09-03 19:15 ` [v10,net-next,1/2] " netdev-bot+sashiko
2026-08-31 13:10 ` [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-09-02 1:39 ` [PATCH v10 net-next 0/2] octeontx2-pf: " Ratheesh Kannoth
2 siblings, 1 reply; 7+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:10 UTC (permalink / raw)
To: bpf, linux-kernel, netdev
Cc: andrew+netdev, ast, daniel, davem, edumazet, hawk, horms, jhs,
jiri, john.fastabend, kuba, pabeni, sdf, sgoutham,
Ratheesh Kannoth
Extend struct tc_mqprio_qopt_offload with the owning qdisc handle and
fill it from mqprio_enable_offload() and mqprio_disable_offload().
Offload drivers need the handle to tell apart mqprio setup, teardown,
and tc qdisc replace when they track committed configuration state.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
include/net/pkt_sched.h | 1 +
net/sched/sch_mqprio.c | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/include/net/pkt_sched.h b/include/net/pkt_sched.h
index 90d3e7943b19..9f8694ecd436 100644
--- a/include/net/pkt_sched.h
+++ b/include/net/pkt_sched.h
@@ -165,6 +165,7 @@ struct tc_mqprio_caps {
struct tc_mqprio_qopt_offload {
/* struct tc_mqprio_qopt must always be the first element */
struct tc_mqprio_qopt qopt;
+ u32 handle;
struct netlink_ext_ack *extack;
u16 mode;
u16 shaper;
diff --git a/net/sched/sch_mqprio.c b/net/sched/sch_mqprio.c
index 6ced7008ef5c..adc1393f84ea 100644
--- a/net/sched/sch_mqprio.c
+++ b/net/sched/sch_mqprio.c
@@ -39,6 +39,7 @@ static int mqprio_enable_offload(struct Qdisc *sch,
struct net_device *dev = qdisc_dev(sch);
struct tc_mqprio_qopt_offload mqprio = {
.qopt = *qopt,
+ .handle = sch->handle,
.extack = extack,
};
int err, i;
@@ -79,7 +80,9 @@ static int mqprio_enable_offload(struct Qdisc *sch,
static void mqprio_disable_offload(struct Qdisc *sch)
{
- struct tc_mqprio_qopt_offload mqprio = { { 0 } };
+ struct tc_mqprio_qopt_offload mqprio = {
+ .handle = sch->handle,
+ };
struct mqprio_sched *priv = qdisc_priv(sch);
struct net_device *dev = qdisc_dev(sch);
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
2026-08-31 13:10 [PATCH v10 net-next 0/2] octeontx2-pf: mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-08-31 13:10 ` [PATCH v10 net-next 1/2] net/sched: mqprio: pass qdisc handle to offload drivers Ratheesh Kannoth
@ 2026-08-31 13:10 ` Ratheesh Kannoth
2026-09-01 13:11 ` sashiko-bot
2026-09-03 19:15 ` [v10,net-next,2/2] " netdev-bot+sashiko
2026-09-02 1:39 ` [PATCH v10 net-next 0/2] octeontx2-pf: " Ratheesh Kannoth
2 siblings, 2 replies; 7+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:10 UTC (permalink / raw)
To: bpf, linux-kernel, netdev
Cc: andrew+netdev, ast, daniel, davem, edumazet, hawk, horms, jhs,
jiri, john.fastabend, kuba, pabeni, sdf, sgoutham,
Ratheesh Kannoth
Add TC_SETUP_QDISC_MQPRIO handling for channel-mode mqprio with
TC_MQPRIO_SHAPER_BW_RATE. Shape each non-QoS transmit queue by
programming MDQ CIR/PIR on hw.non_qos_queues. When bandwidth offload is
enabled, allocate one SMQ per such queue, parent every MDQ under TL4[0],
and apply each traffic class min/max rate to the queue(s) in that class.
Reprogram the default TX scheduler hierarchy through ndo_stop() and
ndo_open() on every mqprio add, replace, or delete.
Track committed mqprio state in mq_offload_snap snapshots (handle, TC
layout, and rates) using the qdisc handle passed from the core mqprio
offload path. Roll back from the previous snapshot when tc qdisc replace
fails, and ignore teardown from a replaced qdisc instance once the new
handle is committed.
Restore shapers from the cached per-queue rates in otx2_mqprio_up()
during ndo_open(). Log and continue if programming fails so a routine
link up is not blocked.
Reject offload unless the interface is running and the device supports
CIR+PIR shaping. Reject per-TC rates for traffic classes spanning more
than one queue, SDP rep devices, and concurrent PFC or XDP use. Block
ethtool channel count changes while mqprio bandwidth offload is active.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../ethernet/marvell/octeontx2/af/rvu_nix.c | 6 +-
.../marvell/octeontx2/nic/otx2_common.c | 145 +++-
.../marvell/octeontx2/nic/otx2_common.h | 27 +
.../marvell/octeontx2/nic/otx2_dcbnl.c | 6 +
.../marvell/octeontx2/nic/otx2_ethtool.c | 8 +
.../ethernet/marvell/octeontx2/nic/otx2_pf.c | 12 +
.../ethernet/marvell/octeontx2/nic/otx2_tc.c | 696 ++++++++++++++++++
.../net/ethernet/marvell/octeontx2/nic/qos.c | 3 +
8 files changed, 901 insertions(+), 2 deletions(-)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
index 153eb57bad06..c9a4326d725d 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
@@ -331,8 +331,12 @@ static bool is_valid_txschq(struct rvu *rvu, int blkaddr,
return true;
}
- if (map_func != pcifunc)
+ if (map_func != pcifunc) {
+ dev_err_ratelimited(rvu->dev,
+ "pcifunc %x map pcifunc %x not equal, lvl=%u schq=%u\n",
+ pcifunc, map_func, lvl, schq);
return false;
+ }
return true;
}
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
index 175992188c18..5c502c9d7c83 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
@@ -615,6 +615,142 @@ void otx2_get_mac_from_af(struct net_device *netdev)
}
EXPORT_SYMBOL(otx2_get_mac_from_af);
+static int
+otx2_nix_tmq_reg_write(struct otx2_nic *pfvf, int cnt,
+ u64 reg_addr[MAX_REGS_PER_MBOX_MSG],
+ u64 reg_val[MAX_REGS_PER_MBOX_MSG])
+{
+ struct mbox *mbox = &pfvf->mbox;
+ struct nix_txschq_config *req;
+ int i, err;
+
+ mutex_lock(&mbox->lock);
+ req = otx2_mbox_alloc_msg_nix_txschq_cfg(mbox);
+ if (!req) {
+ mutex_unlock(&mbox->lock);
+ return -ENOMEM;
+ }
+
+ req->lvl = NIX_TXSCH_LVL_MDQ;
+ req->num_regs = cnt;
+
+ for (i = 0; i < cnt; i++) {
+ req->reg[i] = reg_addr[i];
+ req->regval[i] = reg_val[i];
+ }
+
+ err = otx2_sync_mbox_msg(mbox);
+ mutex_unlock(&mbox->lock);
+
+ return err;
+}
+
+int otx2_nix_tm_clear_queue_shaper(struct otx2_nic *pfvf)
+{
+ u64 reg_addr[MAX_REGS_PER_MBOX_MSG];
+ u64 reg_val[MAX_REGS_PER_MBOX_MSG];
+ int err, smq, i, cnt = 0;
+
+ for (i = 0; i < pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_SMQ]; i++) {
+ smq = pfvf->hw.txschq_list[NIX_TXSCH_LVL_SMQ][i];
+
+ reg_addr[cnt] = NIX_AF_MDQX_PIR(smq);
+ reg_val[cnt] = 0;
+ cnt++;
+
+ reg_addr[cnt] = NIX_AF_MDQX_CIR(smq);
+ reg_val[cnt] = 0;
+ cnt++;
+
+ if (cnt < MAX_REGS_PER_MBOX_MSG - 1)
+ continue;
+
+ err = otx2_nix_tmq_reg_write(pfvf, cnt,
+ reg_addr, reg_val);
+ if (err)
+ goto fail;
+ cnt = 0;
+ }
+
+ if (cnt) {
+ err = otx2_nix_tmq_reg_write(pfvf, cnt,
+ reg_addr, reg_val);
+ if (err)
+ goto fail;
+ }
+
+ return 0;
+fail:
+ return err;
+}
+
+int otx2_nix_tm_set_queue_shaper(struct otx2_nic *pfvf,
+ int txq, u64 minrate, u64 maxrate)
+{
+ struct mbox *mbox = &pfvf->mbox;
+ struct nix_txschq_config *req;
+ int err, smq, n = 0;
+ u64 reg_addr[2];
+ u64 reg_val[2];
+ u64 rate;
+
+ if (!maxrate && !minrate) {
+ smq = otx2_get_smq_idx(pfvf, txq);
+ reg_addr[0] = NIX_AF_MDQX_PIR(smq);
+ reg_val[0] = 0;
+ reg_addr[1] = NIX_AF_MDQX_CIR(smq);
+ reg_val[1] = 0;
+ return otx2_nix_tmq_reg_write(pfvf, 2, reg_addr, reg_val);
+ }
+
+ smq = otx2_get_smq_idx(pfvf, txq);
+
+ mutex_lock(&mbox->lock);
+ req = otx2_mbox_alloc_msg_nix_txschq_cfg(mbox);
+ if (!req) {
+ mutex_unlock(&mbox->lock);
+ return -ENOMEM;
+ }
+
+ req->lvl = NIX_TXSCH_LVL_MDQ;
+
+ /* MQPRIO exposes only min/max rate, not burst. Pass burst 0 so
+ * otx2_get_egress_burst_cfg() programmes the largest burst the NIX
+ * encoding supports (CN10K_MAX_BURST_SIZE on CN10K). This differs
+ * from the 65536 byte default used in the HTB path, which is a
+ * kernel-side default when no explicit burst is configured, not a
+ * hardware cap.
+ *
+ * mqprio setup restarts the netdev (otx2_mqprio_restart_netdev),
+ * which resets MDQ shapers to zero. Program both PIR and CIR on
+ * every update so omitted rates are applied explicitly rather than
+ * relying on stale hardware state.
+ */
+ req->reg[n] = NIX_AF_MDQX_PIR(smq);
+ if (maxrate) {
+ rate = otx2_convert_rate(maxrate);
+ req->regval[n] = otx2_get_txschq_rate_regval(pfvf, rate, 0);
+ } else {
+ req->regval[n] = 0;
+ }
+ n++;
+
+ /* CIR+PIR support is required and checked at mqprio setup. */
+ req->reg[n] = NIX_AF_MDQX_CIR(smq);
+ if (minrate) {
+ rate = otx2_convert_rate(minrate);
+ req->regval[n] = otx2_get_txschq_rate_regval(pfvf, rate, 0);
+ } else {
+ req->regval[n] = 0;
+ }
+ n++;
+ req->num_regs = n;
+
+ err = otx2_sync_mbox_msg(mbox);
+ mutex_unlock(&mbox->lock);
+ return err;
+}
+
int otx2_txschq_config(struct otx2_nic *pfvf, int lvl, int prio, bool txschq_for_pfc)
{
u16 (*schq_list)[MAX_TXSCHQ_PER_FUNC];
@@ -651,7 +787,11 @@ int otx2_txschq_config(struct otx2_nic *pfvf, int lvl, int prio, bool txschq_for
(u64)hw->smq_link_type);
req->num_regs++;
/* MDQ config */
- parent = schq_list[NIX_TXSCH_LVL_TL4][prio];
+ if (pfvf->mqprio.rate_limit)
+ parent = schq_list[NIX_TXSCH_LVL_TL4][0];
+ else
+ parent = schq_list[NIX_TXSCH_LVL_TL4][prio];
+
req->reg[1] = NIX_AF_MDQX_PARENT(schq);
req->regval[1] = parent << 16;
req->num_regs++;
@@ -779,6 +919,9 @@ int otx2_txsch_alloc(struct otx2_nic *pfvf)
req->schq[NIX_TXSCH_LVL_TL4] = chan_cnt;
}
+ if (pfvf->mqprio.rate_limit)
+ req->schq[NIX_TXSCH_LVL_SMQ] = pfvf->hw.non_qos_queues;
+
rc = otx2_sync_mbox_msg(&pfvf->mbox);
if (rc)
return rc;
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h
index eecee612b7b2..dbd46ff26d96 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h
@@ -17,6 +17,7 @@
#include <linux/soc/marvell/silicons.h>
#include <linux/soc/marvell/octeontx2/asm.h>
#include <net/macsec.h>
+#include <uapi/linux/pkt_sched.h>
#include <net/pkt_cls.h>
#include <net/devlink.h>
#include <linux/time64.h>
@@ -483,6 +484,23 @@ struct pf_irq_data {
int mdevs;
};
+struct mq_offload_snap {
+ u32 handle;
+ u64 min_rate[TC_QOPT_MAX_QUEUE];
+ u64 max_rate[TC_QOPT_MAX_QUEUE];
+ __u8 num_tc;
+ __u16 count[TC_QOPT_MAX_QUEUE];
+ __u16 offset[TC_QOPT_MAX_QUEUE];
+};
+
+struct otx2_mqprio {
+ u32 flags;
+ u32 handle;
+ u64 *min_rate;
+ u64 *max_rate;
+ bool rate_limit;
+};
+
struct otx2_nic {
void __iomem *reg_base;
struct net_device *netdev;
@@ -515,6 +533,10 @@ struct otx2_nic {
u64 flags;
u64 *cq_op_addr;
+ struct otx2_mqprio mqprio;
+ struct mq_offload_snap *cur_mq_snap;
+ struct mq_offload_snap *old_mq_snap;
+
struct bpf_prog *xdp_prog;
struct otx2_qset qset;
struct otx2_hw hw;
@@ -1246,6 +1268,11 @@ dma_addr_t otx2_dma_map_skb_frag(struct otx2_nic *pfvf,
struct sk_buff *skb, int seg, int *len);
void otx2_dma_unmap_skb_frags(struct otx2_nic *pfvf, struct sg_list *sg);
int otx2_read_free_sqe(struct otx2_nic *pfvf, u16 qidx);
+int otx2_nix_tm_set_queue_shaper(struct otx2_nic *pfvf, int txq,
+ u64 minrate, u64 maxrate);
+int otx2_nix_tm_clear_queue_shaper(struct otx2_nic *pfvf);
+int otx2_mqprio_down(struct otx2_nic *pfvf);
+int otx2_mqprio_up(struct otx2_nic *pfvf);
void otx2_queue_vf_work(struct mbox *mw, struct workqueue_struct *mbox_wq,
int first, int mdevs, u64 intr);
int otx2_del_mcam_flow_entry(struct otx2_nic *nic, u16 entry,
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_dcbnl.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_dcbnl.c
index f110dfa42360..4a70abc230be 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_dcbnl.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_dcbnl.c
@@ -413,6 +413,12 @@ static int otx2_dcbnl_ieee_setpfc(struct net_device *dev, struct ieee_pfc *pfc)
u8 old_pfc_en;
int err;
+ if (pfvf->mqprio.rate_limit && pfc->pfc_en) {
+ netdev_err(dev,
+ "PFC: cannot enable while mqprio bandwidth offload is active\n");
+ return -EOPNOTSUPP;
+ }
+
old_pfc_en = pfvf->pfc_en;
pfvf->pfc_en = pfc->pfc_en;
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_ethtool.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_ethtool.c
index 9bee1b91eeaa..ec2601c6c255 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_ethtool.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_ethtool.c
@@ -287,6 +287,14 @@ static int otx2_set_channels(struct net_device *dev,
return -EINVAL;
}
+ if (pfvf->mqprio.rate_limit &&
+ (channel->tx_count != pfvf->hw.tx_queues ||
+ channel->rx_count != pfvf->hw.rx_queues)) {
+ netdev_info(dev,
+ "Not permitted to change channel count while MQ prio is active\n");
+ return -EINVAL;
+ }
+
if (if_up)
dev->netdev_ops->ndo_stop(dev);
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
index c995f2900859..79a82e445454 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
@@ -1980,6 +1980,12 @@ int otx2_open(struct net_device *netdev)
if (err)
goto err_free_mem;
+ err = otx2_mqprio_up(pf);
+ if (err)
+ netdev_err(pf->netdev,
+ "mqprio: failed to restore shapers during open: %d; continuing without bandwidth limits\n",
+ err);
+
/* Register NAPI handler */
for (qidx = 0; qidx < pf->hw.cint_cnt; qidx++) {
cq_poll = &qset->napi[qidx];
@@ -2846,6 +2852,12 @@ static int otx2_xdp_setup(struct otx2_nic *pf, struct bpf_prog *prog)
bool if_up = netif_running(pf->netdev);
struct bpf_prog *old_prog;
+ if (prog && pf->mqprio.rate_limit) {
+ netdev_err(dev,
+ "XDP: cannot attach while mqprio bandwidth offload is active\n");
+ return -EOPNOTSUPP;
+ }
+
if (prog && dev->mtu > MAX_XDP_MTU) {
netdev_warn(dev, "Jumbo frames not yet supported with XDP\n");
return -EOPNOTSUPP;
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
index 039fd47ebf52..3f835eb977d3 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
@@ -16,6 +16,8 @@
#include <net/tc_act/tc_mirred.h>
#include <net/tc_act/tc_vlan.h>
#include <net/ipv6.h>
+#include <net/pkt_sched.h>
+#include <net/sch_generic.h>
#include "cn10k.h"
#include "otx2_common.h"
@@ -31,6 +33,10 @@
#define MCAST_INVALID_GRP (-1U)
#define RATE_MANTISSA_BITS 8
+/* Min per-queue egress shaping rate the NIX TLX encoder supports (2 Mbps). */
+#define OTX2_MQPRIO_MIN_RATE_BYTES_PS 250000ULL
+/* Max egress shaping rate the NIX TLX encoder supports (130816 Mbps). */
+#define OTX2_MQPRIO_MAX_RATE_BYTES_PS ((MAX_BURST_SIZE * 1000000ULL) / 8ULL)
static void otx2_get_egress_burst_cfg(struct otx2_nic *nic, u32 burst,
u32 *burst_exp, u32 *burst_mantissa)
@@ -61,6 +67,9 @@ static void otx2_get_egress_burst_cfg(struct otx2_nic *nic, u32 burst,
*burst_mantissa = tmp / (1ULL << (*burst_exp - 7));
}
} else {
+ /* burst 0: largest encodable burst (CN10K_MAX_BURST_SIZE on
+ * CN10K), not a minimal burst.
+ */
*burst_exp = MAX_BURST_EXPONENT;
*burst_mantissa = max_mantissa;
}
@@ -1600,14 +1609,700 @@ static int otx2_setup_tc_block(struct net_device *netdev,
nic, nic, ingress);
}
+/* Free the per-queue min/max rate caches. */
+static void otx2_mqprio_free_cache(struct otx2_nic *pfvf)
+{
+ devm_kfree(pfvf->dev, pfvf->mqprio.min_rate);
+ devm_kfree(pfvf->dev, pfvf->mqprio.max_rate);
+ pfvf->mqprio.min_rate = NULL;
+ pfvf->mqprio.max_rate = NULL;
+ pfvf->mqprio.flags = 0;
+}
+
+static int otx2_mqprio_alloc_cache(struct otx2_nic *pfvf, bool replacing)
+{
+ u16 num_txq = pfvf->hw.non_qos_queues;
+
+ if (replacing && pfvf->mqprio.min_rate && pfvf->mqprio.max_rate) {
+ memset(pfvf->mqprio.min_rate, 0,
+ num_txq * sizeof(*pfvf->mqprio.min_rate));
+ memset(pfvf->mqprio.max_rate, 0,
+ num_txq * sizeof(*pfvf->mqprio.max_rate));
+ pfvf->mqprio.flags = 0;
+ return 0;
+ }
+
+ otx2_mqprio_free_cache(pfvf);
+
+ pfvf->mqprio.min_rate = devm_kcalloc(pfvf->dev, num_txq,
+ sizeof(*pfvf->mqprio.min_rate),
+ GFP_KERNEL);
+ pfvf->mqprio.max_rate = devm_kcalloc(pfvf->dev, num_txq,
+ sizeof(*pfvf->mqprio.max_rate),
+ GFP_KERNEL);
+ if (!pfvf->mqprio.min_rate || !pfvf->mqprio.max_rate) {
+ otx2_mqprio_free_cache(pfvf);
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static void otx2_mqprio_snap_free(struct otx2_nic *pfvf,
+ struct mq_offload_snap **snap)
+{
+ if (!*snap)
+ return;
+
+ devm_kfree(pfvf->dev, *snap);
+ *snap = NULL;
+}
+
+static int otx2_mqprio_snap_copy(struct otx2_nic *pfvf,
+ struct mq_offload_snap **dst,
+ const struct tc_mqprio_qopt_offload *mqprio)
+{
+ const struct tc_mqprio_qopt *qopt = &mqprio->qopt;
+ struct mq_offload_snap *snap;
+ int tc;
+
+ if (!*dst) {
+ snap = devm_kzalloc(pfvf->dev, sizeof(*snap), GFP_KERNEL);
+ if (!snap)
+ return -ENOMEM;
+ *dst = snap;
+ } else {
+ snap = *dst;
+ }
+
+ snap->handle = mqprio->handle;
+ snap->num_tc = qopt->num_tc;
+ for (tc = 0; tc < TC_QOPT_MAX_QUEUE; tc++) {
+ snap->count[tc] = qopt->count[tc];
+ snap->offset[tc] = qopt->offset[tc];
+ snap->min_rate[tc] = 0;
+ snap->max_rate[tc] = 0;
+ }
+
+ for (tc = 0; tc < qopt->num_tc; tc++) {
+ if (mqprio->flags & TC_MQPRIO_F_MIN_RATE)
+ snap->min_rate[tc] = mqprio->min_rate[tc];
+ if (mqprio->flags & TC_MQPRIO_F_MAX_RATE)
+ snap->max_rate[tc] = mqprio->max_rate[tc];
+ }
+
+ return 0;
+}
+
+static int otx2_mqprio_stage_cur(struct otx2_nic *pfvf,
+ const struct tc_mqprio_qopt_offload *mqprio)
+{
+ return otx2_mqprio_snap_copy(pfvf, &pfvf->cur_mq_snap, mqprio);
+}
+
+static void otx2_mqprio_snap_commit(struct otx2_nic *pfvf)
+{
+ otx2_mqprio_snap_free(pfvf, &pfvf->old_mq_snap);
+ pfvf->old_mq_snap = pfvf->cur_mq_snap;
+ pfvf->cur_mq_snap = NULL;
+}
+
+static bool otx2_mqprio_mdq_allocated(struct otx2_nic *pfvf)
+{
+ return pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_MDQ] != 0;
+}
+
+static int otx2_mqprio_restore_old(struct otx2_nic *pfvf)
+{
+ struct mq_offload_snap *snap = pfvf->old_mq_snap;
+ struct net_device *netdev = pfvf->netdev;
+ u16 num_txq = pfvf->hw.non_qos_queues;
+ int tc, txq, err;
+
+ if (!snap)
+ return 0;
+
+ err = otx2_mqprio_alloc_cache(pfvf, false);
+ if (err)
+ return err;
+
+ memset(pfvf->mqprio.min_rate, 0, num_txq * sizeof(*pfvf->mqprio.min_rate));
+ memset(pfvf->mqprio.max_rate, 0, num_txq * sizeof(*pfvf->mqprio.max_rate));
+ pfvf->mqprio.flags = 0;
+
+ for (tc = 0; tc < snap->num_tc; tc++) {
+ u64 min_rate = snap->min_rate[tc];
+ u64 max_rate = snap->max_rate[tc];
+
+ if (min_rate)
+ pfvf->mqprio.flags |= TC_MQPRIO_F_MIN_RATE;
+ if (max_rate)
+ pfvf->mqprio.flags |= TC_MQPRIO_F_MAX_RATE;
+
+ for (txq = snap->offset[tc];
+ txq < snap->offset[tc] + snap->count[tc]; txq++) {
+ pfvf->mqprio.min_rate[txq] = min_rate;
+ pfvf->mqprio.max_rate[txq] = max_rate;
+ }
+ }
+
+ pfvf->mqprio.handle = snap->handle;
+
+ netdev_set_num_tc(netdev, snap->num_tc);
+ for (tc = 0; tc < snap->num_tc; tc++)
+ netdev_set_tc_queue(netdev, tc, snap->count[tc],
+ snap->offset[tc]);
+
+ if (otx2_mqprio_mdq_allocated(pfvf)) {
+ err = otx2_nix_tm_clear_queue_shaper(pfvf);
+ if (err)
+ return err;
+ }
+
+ err = otx2_mqprio_up(pfvf);
+ if (err)
+ return err;
+
+ otx2_mqprio_snap_free(pfvf, &pfvf->cur_mq_snap);
+
+ return 0;
+}
+
+static void otx2_mqprio_snap_destroy(struct otx2_nic *pfvf)
+{
+ otx2_mqprio_snap_free(pfvf, &pfvf->cur_mq_snap);
+ otx2_mqprio_snap_free(pfvf, &pfvf->old_mq_snap);
+}
+
+static void otx2_mqprio_clear_sw(struct otx2_nic *pfvf)
+{
+ struct net_device *netdev = pfvf->netdev;
+
+ pfvf->mqprio.rate_limit = false;
+ pfvf->mqprio.handle = 0;
+ netdev_set_num_tc(netdev, 0);
+ otx2_mqprio_free_cache(pfvf);
+}
+
+/* Tear down mqprio bandwidth offload: clear per-queue shapers,
+ * mqprio_rate_limit, netdev TC mappings, and the cached rates. Called on
+ * explicit mqprio teardown (tc qdisc del) and error cleanup, not on
+ * routine netdev stop/open cycles where the offload stays active.
+ */
+int otx2_mqprio_down(struct otx2_nic *pfvf)
+{
+ int err = 0;
+
+ if (!pfvf->mqprio.rate_limit)
+ return 0;
+
+ if (netif_running(pfvf->netdev) &&
+ otx2_mqprio_mdq_allocated(pfvf))
+ err = otx2_nix_tm_clear_queue_shaper(pfvf);
+
+ if (err) {
+ netdev_err(pfvf->netdev,
+ "mqprio: failed to clear hardware shapers: %d; some TX queues may retain bandwidth limits\n",
+ err);
+ return err;
+ }
+
+ otx2_mqprio_clear_sw(pfvf);
+
+ return 0;
+}
+
+int otx2_mqprio_up(struct otx2_nic *pfvf)
+{
+ struct net_device *netdev = pfvf->netdev;
+ int txq, err;
+
+ if (!pfvf->mqprio.rate_limit)
+ return 0;
+
+ if (!pfvf->mqprio.min_rate || !pfvf->mqprio.max_rate)
+ return 0;
+
+ for (txq = 0; txq < pfvf->hw.non_qos_queues; txq++) {
+ u64 min_rate = 0, max_rate = 0;
+
+ if (pfvf->mqprio.flags & TC_MQPRIO_F_MIN_RATE)
+ min_rate = pfvf->mqprio.min_rate[txq];
+ if (pfvf->mqprio.flags & TC_MQPRIO_F_MAX_RATE)
+ max_rate = pfvf->mqprio.max_rate[txq];
+
+ if (!min_rate && !max_rate)
+ continue;
+
+ err = otx2_nix_tm_set_queue_shaper(pfvf, txq, min_rate,
+ max_rate);
+ if (err) {
+ netdev_err(netdev,
+ "mqprio: failed to restore shaper for txq %d: %d\n",
+ txq, err);
+ return err;
+ }
+ }
+
+ return 0;
+}
+
+/* Restart the netdev to reprogram the TX scheduler hierarchy for mqprio
+ * bandwidth offload. Both mqprio add and delete (when offload was active)
+ * take this path via ndo_stop()/ndo_open() so VF-specific open logic (e.g.
+ * LBK carrier on) runs correctly. The full stop/open cycle clears
+ * carrier, stops all TX queues, tears down IRQs/NAPI and drops in-flight
+ * traffic. If open fails, the interface is left administratively down
+ * without calling ndo_stop() again on resources already torn down by
+ * the open error path.
+ *
+ * Do not call dev_deactivate()/dev_activate() here: this runs from
+ * ndo_setup_tc() while qdisc_graft() may already hold the device
+ * deactivated and must perform the final dev_activate().
+ */
+static int otx2_mqprio_restart_netdev(struct net_device *netdev, bool rate_limit)
+{
+ struct otx2_nic *pfvf = netdev_priv(netdev);
+ const struct net_device_ops *ops = netdev->netdev_ops;
+ int err;
+
+ /* TODO: Explore live TX scheduler reprogramming to avoid a full
+ * ndo_stop()/ndo_open() bounce on every mqprio change.
+ */
+ netdev_info(netdev,
+ "mqprio: restarting interface to reprogram TX scheduler; in-flight traffic will be dropped\n");
+
+ err = ops->ndo_stop(netdev);
+ if (err)
+ return err;
+
+ /* Set before ndo_open() so otx2_txsch_alloc() widens SMQ allocation. */
+ if (rate_limit)
+ pfvf->mqprio.rate_limit = true;
+
+ err = ops->ndo_open(netdev);
+ if (err) {
+ netdev_err(netdev,
+ "Failed to restart device after mqprio change: %d\n",
+ err);
+ /* ndo_open() already freed the TX schedulers on failure while
+ * netif_running() may still be true; drop mqprio software state
+ * only instead of sending shaper clears to freed queues.
+ */
+ otx2_mqprio_clear_sw(pfvf);
+ /* ndo_open() rolls back on failure; mark the interface down so
+ * netif_close() does not invoke ndo_stop() on freed NAPI/queue
+ * state. Caller holds RTNL; dev_close() would deadlock.
+ */
+ pfvf->flags |= OTX2_FLAG_INTF_DOWN;
+ /* visible to otx2_stop() on other cpus */
+ smp_wmb();
+ netif_close(netdev);
+ }
+
+ return err;
+}
+
+static int otx2_mqprio_validate_tc_rate(struct net_device *netdev,
+ struct netlink_ext_ack *extack,
+ u64 rate, u32 qcount, int tc,
+ const char *name)
+{
+ if (!rate)
+ return 0;
+
+ if (qcount <= 1)
+ return 0;
+
+ /* TODO: mqprio min_rate/max_rate are per traffic class, but bandwidth
+ * offload shapes on per-queue MDQ nodes parented under a single TL4.
+ * Without per-TC TL4 shapers the driver cannot honor TC-level limits
+ * for a traffic class that spans multiple queues without either
+ * dividing the rate across queues (uAPI mismatch) or exceeding the TC
+ * cap when every member queue is active. Reject until per-TC TL4
+ * shaping can be implemented without allocating additional TL4 nodes
+ * beyond the existing hierarchy.
+ */
+ netdev_err(netdev,
+ "mqprio: %s rate for tc %d not supported with %u queues\n",
+ name, tc, qcount);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "mqprio: %s rate for tc %d not supported with %u queues",
+ name, tc, qcount);
+ return -EOPNOTSUPP;
+}
+
+static int otx2_mqprio_validate_txqs(struct net_device *netdev,
+ struct netlink_ext_ack *extack,
+ struct tc_mqprio_qopt *qopt)
+{
+ struct otx2_nic *pfvf = netdev_priv(netdev);
+ u16 num_txq = pfvf->hw.non_qos_queues;
+ int tc, txq;
+
+ if (qopt->num_tc > num_txq) {
+ netdev_err(netdev, "Number of TCs (%u) exceeds hw queues %u\n",
+ qopt->num_tc, num_txq);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "Number of TCs (%u) exceeds hw queues %u",
+ qopt->num_tc, num_txq);
+ return -EINVAL;
+ }
+
+ if (num_txq > MAX_TXSCHQ_PER_FUNC) {
+ netdev_err(netdev,
+ "Number of queues (%u) exceeds max scheduler queues %u\n",
+ num_txq, MAX_TXSCHQ_PER_FUNC);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "Number of queues (%u) exceeds max scheduler queues %u",
+ num_txq, MAX_TXSCHQ_PER_FUNC);
+ return -EINVAL;
+ }
+
+ for (tc = 0; tc < qopt->num_tc; tc++) {
+ u32 qcount = qopt->count[tc];
+
+ for (txq = qopt->offset[tc];
+ txq < qopt->offset[tc] + qcount; txq++) {
+ if (txq >= num_txq) {
+ netdev_err(netdev,
+ "mqprio: txq %d exceeds offload queue count %u\n",
+ txq, num_txq);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "mqprio: txq %d exceeds offload queue count %u",
+ txq, num_txq);
+ return -EINVAL;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static bool otx2_mqprio_rate_valid(u64 rate_bytes_ps)
+{
+ u64 mbps;
+
+ if (!rate_bytes_ps)
+ return true;
+
+ if (rate_bytes_ps < OTX2_MQPRIO_MIN_RATE_BYTES_PS)
+ return false;
+
+ if (rate_bytes_ps > OTX2_MQPRIO_MAX_RATE_BYTES_PS)
+ return false;
+
+ if (rate_bytes_ps > div_u64(U64_MAX, 8))
+ return false;
+
+ mbps = otx2_convert_rate(rate_bytes_ps);
+ return ilog2(mbps / 2) <= MAX_RATE_EXPONENT;
+}
+
+static int otx2_teardown_tc_mqprio(struct otx2_nic *pfvf,
+ struct tc_mqprio_qopt_offload *mqprio)
+{
+ bool had_mqprio = pfvf->mqprio.rate_limit;
+ struct tc_mqprio_qopt *qopt = &mqprio->qopt;
+ struct net_device *netdev = pfvf->netdev;
+ bool if_up = netif_running(netdev);
+ bool rollback;
+ int err;
+
+ qopt->hw = 0;
+
+ /* tc qdisc replace runs setup on the new mqprio before destroying the
+ * old one. Ignore teardown from the replaced instance once the new
+ * handle is committed in otx2_setup_tc_mqprio().
+ */
+ if (mqprio->handle && mqprio->handle != pfvf->mqprio.handle) {
+ if (pfvf->cur_mq_snap)
+ otx2_mqprio_snap_commit(pfvf);
+ return 0;
+ }
+
+ if (mqprio->handle && mqprio->handle == pfvf->mqprio.handle &&
+ pfvf->old_mq_snap) {
+ rollback = pfvf->cur_mq_snap;
+
+ err = otx2_mqprio_restore_old(pfvf);
+ if (err)
+ return err;
+ if (rollback)
+ return 0;
+ }
+
+ /* Skip the netdev restart when mqprio offload was not active. */
+ if (!had_mqprio)
+ return 0;
+
+ if (if_up) {
+ int down_err, err;
+
+ down_err = otx2_mqprio_down(pfvf);
+ err = otx2_mqprio_restart_netdev(netdev, false);
+ if (err)
+ return err;
+ return down_err;
+ }
+
+ /* ndo_stop() already freed the TX scheduler TL nodes; drop software
+ * state only.
+ */
+ otx2_mqprio_clear_sw(pfvf);
+ return 0;
+}
+
+static int otx2_setup_tc_mqprio(struct net_device *netdev,
+ struct tc_mqprio_qopt_offload *mqprio)
+{
+ struct otx2_nic *pfvf = netdev_priv(netdev);
+ struct tc_mqprio_qopt *qopt = &mqprio->qopt;
+ struct netlink_ext_ack *extack = mqprio->extack;
+ bool replacing = pfvf->mqprio.rate_limit;
+ bool if_up = netif_running(netdev);
+ int tc, txq, err, i;
+
+ if (!qopt->hw)
+ return otx2_teardown_tc_mqprio(pfvf, mqprio);
+
+ if (!if_up) {
+ netdev_err(netdev, "mqprio: setup requires interface UP\n");
+ NL_SET_ERR_MSG_MOD(extack, "mqprio: setup requires interface UP");
+ return -EOPNOTSUPP;
+ }
+
+ if (mqprio->shaper != TC_MQPRIO_SHAPER_BW_RATE) {
+ netdev_err(netdev, "Unsupported mqprio shaper %#x\n", mqprio->shaper);
+ NL_SET_ERR_MSG_FMT_MOD(extack, "Unsupported mqprio shaper %#x",
+ mqprio->shaper);
+ return -EOPNOTSUPP;
+ }
+
+ if (!test_bit(QOS_CIR_PIR_SUPPORT, &pfvf->hw.cap_flag)) {
+ netdev_err(netdev,
+ "mqprio: bandwidth offload requires CIR+PIR support\n");
+ NL_SET_ERR_MSG_MOD(extack,
+ "mqprio: bandwidth offload requires CIR+PIR support");
+ return -EOPNOTSUPP;
+ }
+
+ if (is_otx2_sdp_rep(pfvf->pdev)) {
+ netdev_err(netdev, "mqprio: bandwidth offload not supported on SDP rep\n");
+ NL_SET_ERR_MSG_MOD(extack,
+ "mqprio: bandwidth offload not supported on SDP rep");
+ return -EOPNOTSUPP;
+ }
+
+ if (pfvf->pfc_en) {
+ netdev_err(netdev,
+ "mqprio: cannot enable offload while PFC is enabled\n");
+ NL_SET_ERR_MSG_MOD(extack,
+ "mqprio: cannot enable offload while PFC is enabled");
+ return -EOPNOTSUPP;
+ }
+
+ if (pfvf->xdp_prog) {
+ netdev_err(netdev,
+ "mqprio: cannot enable offload while XDP is active\n");
+ NL_SET_ERR_MSG_MOD(extack,
+ "mqprio: cannot enable offload while XDP is active");
+ return -EOPNOTSUPP;
+ }
+
+ for (tc = 0; tc < qopt->num_tc; tc++) {
+ u64 min_rate = 0, max_rate = 0;
+ u32 qcount = qopt->count[tc];
+
+ if (mqprio->flags & TC_MQPRIO_F_MIN_RATE)
+ min_rate = mqprio->min_rate[tc];
+ if (mqprio->flags & TC_MQPRIO_F_MAX_RATE)
+ max_rate = mqprio->max_rate[tc];
+
+ if (min_rate && max_rate && min_rate > max_rate) {
+ netdev_err(netdev,
+ "min_rate %llu exceeds max_rate %llu for tc %d\n",
+ min_rate, max_rate, tc);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "min_rate %llu exceeds max_rate %llu for tc %d",
+ min_rate, max_rate, tc);
+ return -EINVAL;
+ }
+
+ if (mqprio->flags & TC_MQPRIO_F_MIN_RATE) {
+ err = otx2_mqprio_validate_tc_rate(netdev, extack, min_rate,
+ qcount, tc, "min");
+ if (err)
+ return err;
+ }
+
+ if (mqprio->flags & TC_MQPRIO_F_MAX_RATE) {
+ err = otx2_mqprio_validate_tc_rate(netdev, extack, max_rate,
+ qcount, tc, "max");
+ if (err)
+ return err;
+ }
+
+ if (mqprio->flags & TC_MQPRIO_F_MIN_RATE &&
+ !otx2_mqprio_rate_valid(min_rate)) {
+ netdev_err(netdev,
+ "mqprio: min_rate %llu for tc %d is outside hardware limits\n",
+ min_rate, tc);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "mqprio: min_rate %llu for tc %d is outside hardware limits",
+ min_rate, tc);
+ return -EINVAL;
+ }
+
+ if (mqprio->flags & TC_MQPRIO_F_MAX_RATE &&
+ !otx2_mqprio_rate_valid(max_rate)) {
+ netdev_err(netdev,
+ "mqprio: max_rate %llu for tc %d is outside hardware limits\n",
+ max_rate, tc);
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "mqprio: max_rate %llu for tc %d is outside hardware limits",
+ max_rate, tc);
+ return -EINVAL;
+ }
+ }
+
+ err = otx2_mqprio_validate_txqs(netdev, extack, qopt);
+ if (err)
+ return err;
+
+ err = otx2_mqprio_stage_cur(pfvf, mqprio);
+ if (err)
+ return err;
+
+ err = otx2_mqprio_restart_netdev(pfvf->netdev, true);
+ if (err)
+ goto cleanup;
+
+ err = otx2_mqprio_alloc_cache(pfvf, replacing);
+ if (err)
+ goto cleanup;
+
+ /* otx2_mqprio_up() may have restored the previous configuration during
+ * the restart above. Clear every MDQ shaper before applying the new
+ * mapping so queues dropped from the TC layout do not keep stale
+ * limits in hardware.
+ */
+ if (otx2_mqprio_mdq_allocated(pfvf)) {
+ err = otx2_nix_tm_clear_queue_shaper(pfvf);
+ if (err)
+ goto cleanup;
+ }
+
+ pfvf->mqprio.flags = mqprio->flags;
+
+ for (tc = 0; tc < qopt->num_tc; tc++) {
+ u64 min_rate = 0, max_rate = 0;
+ u32 qcount = qopt->count[tc];
+
+ /* Rates omitted from tc mqprio are passed as zero and both MDQ
+ * shaper registers are programmed; see
+ * otx2_nix_tm_set_queue_shaper(). Multi-queue TCs with rates
+ * are rejected above.
+ */
+ if (mqprio->flags & TC_MQPRIO_F_MIN_RATE)
+ min_rate = mqprio->min_rate[tc];
+ if (mqprio->flags & TC_MQPRIO_F_MAX_RATE)
+ max_rate = mqprio->max_rate[tc];
+
+ for (txq = qopt->offset[tc];
+ txq < qopt->offset[tc] + qcount; txq++) {
+ netdev_dbg(netdev,
+ "mqprio: tc %d txq %d min_rate %llu max_rate %llu\n",
+ tc, txq, min_rate, max_rate);
+
+ pfvf->mqprio.min_rate[txq] = min_rate;
+ pfvf->mqprio.max_rate[txq] = max_rate;
+
+ err = otx2_nix_tm_set_queue_shaper(pfvf, txq,
+ min_rate, max_rate);
+ if (err)
+ goto cleanup;
+ }
+ }
+
+ netdev_set_num_tc(netdev, pfvf->cur_mq_snap->num_tc);
+ for (i = 0; i < pfvf->cur_mq_snap->num_tc; i++)
+ netdev_set_tc_queue(netdev, i, pfvf->cur_mq_snap->count[i],
+ qopt->offset[i]);
+
+ qopt->hw = TC_MQPRIO_HW_OFFLOAD_TCS;
+ pfvf->mqprio.handle = mqprio->handle;
+
+ if (!replacing)
+ otx2_mqprio_snap_commit(pfvf);
+
+ return 0;
+
+cleanup:
+ qopt->hw = 0;
+ if (replacing) {
+ int restore_err = otx2_mqprio_restore_old(pfvf);
+
+ if (restore_err) {
+ netdev_err(netdev,
+ "mqprio: replace failed and prior configuration rollback failed: %d\n",
+ restore_err);
+ if (extack)
+ NL_SET_ERR_MSG_FMT_MOD(extack,
+ "mqprio: replace failed and prior configuration rollback failed: %d",
+ restore_err);
+ } else {
+ netdev_err(netdev,
+ "mqprio: replace failed; prior configuration restored\n");
+ if (extack)
+ NL_SET_ERR_MSG_MOD(extack,
+ "mqprio: replace failed; prior configuration restored");
+ }
+ return err ? err : -EIO;
+ }
+ otx2_teardown_tc_mqprio(pfvf, mqprio);
+ return err;
+}
+
+static int otx2_setup_tc_root(struct otx2_nic *pfvf,
+ struct tc_root_qopt_offload *root)
+{
+ switch (root->command) {
+ case TC_ROOT_GRAFT:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int otx2_setup_tc_query_caps(void *type_data)
+{
+ struct tc_query_caps_base *base = type_data;
+ struct tc_mqprio_caps *caps;
+
+ if (base->type != TC_SETUP_QDISC_MQPRIO)
+ return -EOPNOTSUPP;
+
+ caps = base->caps;
+ caps->validate_queue_counts = true;
+
+ return 0;
+}
+
int otx2_setup_tc(struct net_device *netdev, enum tc_setup_type type,
void *type_data)
{
switch (type) {
+ case TC_QUERY_CAPS:
+ return otx2_setup_tc_query_caps(type_data);
case TC_SETUP_BLOCK:
return otx2_setup_tc_block(netdev, type_data);
case TC_SETUP_QDISC_HTB:
return otx2_setup_tc_htb(netdev, type_data);
+ case TC_SETUP_QDISC_MQPRIO:
+ return otx2_setup_tc_mqprio(netdev, type_data);
+ case TC_SETUP_ROOT_QDISC:
+ return otx2_setup_tc_root(netdev_priv(netdev), type_data);
default:
return -EOPNOTSUPP;
}
@@ -1632,6 +2327,7 @@ EXPORT_SYMBOL(otx2_init_tc);
void otx2_shutdown_tc(struct otx2_nic *nic)
{
otx2_destroy_tc_flow_list(nic);
+ otx2_mqprio_snap_destroy(nic);
}
EXPORT_SYMBOL(otx2_shutdown_tc);
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/qos.c b/drivers/net/ethernet/marvell/octeontx2/nic/qos.c
index 69c0911e28e9..d9af182e5721 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/qos.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/qos.c
@@ -118,6 +118,9 @@ static void otx2_config_sched_shaping(struct otx2_nic *pfvf,
/* configure PIR */
maxrate = (node->rate > node->ceil) ? node->rate : node->ceil;
+ /* 65536 is the kernel-side default burst when HTB does not supply an
+ * explicit value, not the NIX hardware maximum (CN10K_MAX_BURST_SIZE).
+ */
cfg->regval[*num_regs] =
otx2_get_txschq_rate_regval(pfvf, maxrate, 65536);
(*num_regs)++;
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
2026-08-31 13:10 ` [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
@ 2026-09-01 13:11 ` sashiko-bot
2026-09-03 19:15 ` [v10,net-next,2/2] " netdev-bot+sashiko
1 sibling, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-09-01 13:11 UTC (permalink / raw)
To: Ratheesh Kannoth; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] otx2_nix_tm_set_queue_shaper and otx2_nix_tm_clear_queue_shaper set the mailbox level to NIX_TXSCH_LVL_MDQ while using SMQ indices, leading to guaranteed AF driver validation failures.
- [High] A failure during initial mqprio setup bypasses hardware error cleanup, leaving the device in an inconsistent state.
- [High] A successful `tc qdisc replace` with an identical handle is mistakenly identified as a failure rollback, reverting the hardware configuration.
- [Medium] Failed mqprio replace permanently disables bandwidth offload because the rollback path fails to restore `pfvf->mqprio.rate_limit`.
- [Medium] Memory allocation failure during qdisc deletion aborts teardown, leaking active hardware shapers.
--
commit 3ab815bb73b8a877ec97fa639742f0ab5d81b757
Author: Ratheesh Kannoth <rkannoth@marvell.com>
octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
This commit adds TC_SETUP_QDISC_MQPRIO handling for channel-mode mqprio with
TC_MQPRIO_SHAPER_BW_RATE. It enables shaping each non-QoS transmit queue by
programming MDQ CIR/PIR on hw.non_qos_queues.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
> @@ -615,6 +615,142 @@ void otx2_get_mac_from_af(struct net_device *netdev)
> }
> EXPORT_SYMBOL(otx2_get_mac_from_af);
>
> +static int
> +otx2_nix_tmq_reg_write(struct otx2_nic *pfvf, int cnt,
> + u64 reg_addr[MAX_REGS_PER_MBOX_MSG],
> + u64 reg_val[MAX_REGS_PER_MBOX_MSG])
> +{
> + struct mbox *mbox = &pfvf->mbox;
> + struct nix_txschq_config *req;
> + int i, err;
> +
> + mutex_lock(&mbox->lock);
> + req = otx2_mbox_alloc_msg_nix_txschq_cfg(mbox);
> + if (!req) {
> + mutex_unlock(&mbox->lock);
> + return -ENOMEM;
> + }
> +
> + req->lvl = NIX_TXSCH_LVL_MDQ;
[Severity: High]
Will this cause the AF driver to reject the configuration mailbox messages?
In otx2_nix_tm_set_queue_shaper(), the MDQ register addresses are computed
using the SMQ index. The AF driver's validation function extracts the
index from the register address and validates it against the bounds of the
NIX_TXSCH_LVL_MDQ pool, which has a different size than the SMQ pool.
[ ... ]
> +int otx2_nix_tm_set_queue_shaper(struct otx2_nic *pfvf,
> + int txq, u64 minrate, u64 maxrate)
> +{
[ ... ]
> + req->lvl = NIX_TXSCH_LVL_MDQ;
[Severity: High]
Similar to the concern above, setting the mailbox level to NIX_TXSCH_LVL_MDQ
inline here while relying on SMQ indices appears to guarantee AF driver
validation failures during any qdisc add operation.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
[ ... ]
> +static int otx2_mqprio_restore_old(struct otx2_nic *pfvf)
> +{
[ ... ]
> + if (otx2_mqprio_mdq_allocated(pfvf)) {
> + err = otx2_nix_tm_clear_queue_shaper(pfvf);
> + if (err)
> + return err;
> + }
> +
> + err = otx2_mqprio_up(pfvf);
> + if (err)
> + return err;
[Severity: Medium]
Does this rollback path permanently disable bandwidth offload?
If an ndo_open() failure occurs during tc qdisc replace,
otx2_mqprio_restart_netdev() explicitly clears pfvf->mqprio.rate_limit to
false. Here in otx2_mqprio_restore_old(), the rate_limit flag is not set
back to true before calling otx2_mqprio_up(). As a result, otx2_mqprio_up()
will exit early without configuring the hardware shapers.
[ ... ]
> +static int otx2_teardown_tc_mqprio(struct otx2_nic *pfvf,
> + struct tc_mqprio_qopt_offload *mqprio)
> +{
> + bool had_mqprio = pfvf->mqprio.rate_limit;
> + struct tc_mqprio_qopt *qopt = &mqprio->qopt;
> + struct net_device *netdev = pfvf->netdev;
> + bool if_up = netif_running(netdev);
> + bool rollback;
> + int err;
> +
> + qopt->hw = 0;
> +
> + /* tc qdisc replace runs setup on the new mqprio before destroying the
> + * old one. Ignore teardown from the replaced instance once the new
> + * handle is committed in otx2_setup_tc_mqprio().
> + */
> + if (mqprio->handle && mqprio->handle != pfvf->mqprio.handle) {
> + if (pfvf->cur_mq_snap)
> + otx2_mqprio_snap_commit(pfvf);
> + return 0;
> + }
[Severity: High]
If a fresh qdisc add fails halfway through (for example, during
otx2_nix_tm_set_queue_shaper), could this bypass hardware cleanup?
During a failed initial setup, pfvf->mqprio.handle would still be 0, while
the incoming mqprio->handle from the generic TC layer is non-zero. This
condition would evaluate to true, treating the failure as a post-replace
teardown and returning 0 without performing the actual cleanup.
> +
> + if (mqprio->handle && mqprio->handle == pfvf->mqprio.handle &&
> + pfvf->old_mq_snap) {
> + rollback = pfvf->cur_mq_snap;
> +
> + err = otx2_mqprio_restore_old(pfvf);
> + if (err)
> + return err;
[Severity: High]
Could this inadvertently trigger a rollback during a successful tc qdisc
replace?
If the user issues a replace command utilizing the exact same handle as the
existing qdisc, this condition evaluates to true and appears to call
otx2_mqprio_restore_old(), overriding the newly installed configuration with
the old one.
[Severity: Medium]
If memory allocation fails during a qdisc deletion, will this leak the active
hardware shapers?
When deleting a qdisc, the code falls into this block because the handles
match and old_mq_snap exists. It then calls otx2_mqprio_restore_old(), which
uses devm_kcalloc(). If that allocation fails, the function returns early
here, completely bypassing the actual hardware teardown in otx2_mqprio_down().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260831131014.2639581-1-rkannoth@marvell.com?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v10 net-next 0/2] octeontx2-pf: mqprio bandwidth offload for NIX TX schedulers
2026-08-31 13:10 [PATCH v10 net-next 0/2] octeontx2-pf: mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-08-31 13:10 ` [PATCH v10 net-next 1/2] net/sched: mqprio: pass qdisc handle to offload drivers Ratheesh Kannoth
2026-08-31 13:10 ` [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
@ 2026-09-02 1:39 ` Ratheesh Kannoth
2 siblings, 0 replies; 7+ messages in thread
From: Ratheesh Kannoth @ 2026-09-02 1:39 UTC (permalink / raw)
To: bpf, linux-kernel, netdev
Cc: andrew+netdev, ast, daniel, davem, edumazet, hawk, horms, jhs,
jiri, john.fastabend, kuba, pabeni, sdf, sgoutham
On 2026-08-31 at 18:40:11, Ratheesh Kannoth (rkannoth@marvell.com) wrote:
> This series adds hardware offload for channel-mode mqprio with
> TC_MQPRIO_SHAPER_BW_RATE on Marvell octeontx2 PF devices. Each
> non-QoS transmit queue is shaped by programming MDQ CIR/PIR on the NIX TX
> scheduler. When bandwidth offload is enabled, the driver allocates one
> SMQ per queue, parents every MDQ under TL4[0], and applies each traffic
> class min/max rate to the queue(s) in that class. The TX scheduler
> hierarchy is reprogrammed through ndo_stop()/ndo_open() on every mqprio
> add, replace, or delete.
>
> Patch 1 is required for patch 2. The octeontx2 driver tracks committed
> mqprio configuration in snapshots keyed by the owning qdisc handle. That
> handle is needed to implement two behaviours that patch 2 relies on:
>
> * tc qdisc replace installs the new mqprio and runs its offload setup
> before tearing down the replaced instance. Without the handle, the
> driver cannot tell a stale disable callback from the replaced qdisc
> apart from teardown of the active configuration, and may clear
> hardware shapers or netdev TC state while the new mqprio is being
> committed.
>
> * When replace fails after partial programming, the driver rolls back
> from the previous snapshot. Matching setup and teardown callbacks to
> the same qdisc instance requires the handle passed through the core
> mqprio offload path.
>
> Patch 1 extends struct tc_mqprio_qopt_offload with that handle and fills
> it from mqprio_enable_offload() and mqprio_disable_offload(). Patch 2
> consumes it in the octeontx2 PF mqprio offload path.
>
> The driver rejects offload unless the interface is running and the
> device supports CIR+PIR shaping. Per-TC rates are rejected when a
> traffic class spans more than one queue, and concurrent PFC, XDP, or SDP
> rep use is blocked. Ethtool channel count changes are blocked while mqprio
> bandwidth offload is active.
>
Will address sashiko comments in v11.
pw-bot: changes-requested
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [v10,net-next,1/2] net/sched: mqprio: pass qdisc handle to offload drivers
2026-08-31 13:10 ` [PATCH v10 net-next 1/2] net/sched: mqprio: pass qdisc handle to offload drivers Ratheesh Kannoth
@ 2026-09-03 19:15 ` netdev-bot+sashiko
0 siblings, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:15 UTC (permalink / raw)
To: rkannoth
Cc: bpf, linux-kernel, netdev, andrew+netdev, ast, daniel, davem,
edumazet, hawk, horms, jhs, jiri, john.fastabend, kuba, pabeni,
sdf, sgoutham
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 0 · Low: 2
- [Low] Concern that the new `u32 handle` member of `struct
tc_mqprio_qopt_offload` (include/net/pkt_sched.h:168) is added with no…
- [Low] The new `handle` member of `struct tc_mqprio_qopt_offload` is
populated by only one of the two in-tree producers of that structure.
--
net/sched: mqprio: pass qdisc handle to offload drivers
This adds a u32 handle member to struct tc_mqprio_qopt_offload and fills it
from sch->handle in both mqprio_enable_offload() and mqprio_disable_offload()
so that offload drivers can tell mqprio setup, teardown and qdisc replace
apart. The second patch in the series consumes the field in the octeontx2
driver.
> diff --git a/include/net/pkt_sched.h b/include/net/pkt_sched.h
> index 90d3e7943b19a..9f8694ecd4363 100644
> --- a/include/net/pkt_sched.h
> +++ b/include/net/pkt_sched.h
> @@ -165,6 +165,7 @@ struct tc_mqprio_caps {
> struct tc_mqprio_qopt_offload {
> /* struct tc_mqprio_qopt must always be the first element */
> struct tc_mqprio_qopt qopt;
> + u32 handle;
[Severity: Low]
This isn't a bug, but could the new member get a short comment describing
its contract, next to the existing "struct tc_mqprio_qopt must always be
the first element" note?
The struct is driver facing and shared, and nothing here states that
handle carries the owning mqprio qdisc handle, that it is never
TC_H_UNSPEC when the command comes from sch_mqprio, or what a value of 0
means for a consumer.
> struct netlink_ext_ack *extack;
> u16 mode;
> u16 shaper;
> diff --git a/net/sched/sch_mqprio.c b/net/sched/sch_mqprio.c
> index 6ced7008ef5c8..adc1393f84eaa 100644
> --- a/net/sched/sch_mqprio.c
> +++ b/net/sched/sch_mqprio.c
> @@ -39,6 +39,7 @@ static int mqprio_enable_offload(struct Qdisc *sch,
> struct net_device *dev = qdisc_dev(sch);
> struct tc_mqprio_qopt_offload mqprio = {
> .qopt = *qopt,
> + .handle = sch->handle,
> .extack = extack,
> };
> int err, i;
[Severity: Low]
Should sch_taprio also fill this field? struct tc_mqprio_qopt_offload is
embedded in struct tc_taprio_qopt_offload, and taprio_enable_offload()
populates the embedded copy member by member:
net/sched/sch_taprio.c:taprio_enable_offload() {
...
offload->cmd = TAPRIO_CMD_REPLACE;
offload->extack = extack;
mqprio_qopt_reconstruct(dev, &offload->mqprio.qopt);
offload->mqprio.extack = extack;
taprio_sched_to_offload(dev, sched, offload, &caps);
mqprio_fp_to_offload(q->fp, &offload->mqprio);
...
}
The object comes from the kzalloc in taprio_offload_alloc(), so
offload->mqprio.handle stays 0 (TC_H_UNSPEC), which is a value the
sch_mqprio path never produces.
Several drivers feed that embedded sub-struct into the same handler they
use for TC_SETUP_QDISC_MQPRIO:
felix_vsc9959.c vsc9959_qos_port_tas_set() -> ocelot_port_mqprio()
enetc_qos.c enetc_setup_tc_taprio() -> enetc_setup_tc_mqprio()
am65-cpsw-qos.c am65_cpsw_setup_taprio() -> am65_cpsw_setup_mqprio()
am65_cpsw_setup_mqprio() memcpy-caches the whole struct, so it would cache
handle == 0 on the taprio path and a real handle on the mqprio path for the
same hardware state.
No current consumer is affected: the only reader added by this series is
octeontx2, and otx2_setup_tc() handles TC_QUERY_CAPS, TC_SETUP_BLOCK,
TC_SETUP_QDISC_HTB, TC_SETUP_QDISC_MQPRIO and TC_SETUP_ROOT_QDISC only, so
it never sees a taprio-produced struct.
Would it be better to set offload->mqprio.handle from sch->handle in
taprio_enable_offload() as well, or to document in pkt_sched.h that the
field is only meaningful for TC_SETUP_QDISC_MQPRIO issued by sch_mqprio,
before drivers start keying committed state off it?
> @@ -79,7 +80,9 @@ static int mqprio_enable_offload(struct Qdisc *sch,
>
> static void mqprio_disable_offload(struct Qdisc *sch)
> {
> - struct tc_mqprio_qopt_offload mqprio = { { 0 } };
> + struct tc_mqprio_qopt_offload mqprio = {
> + .handle = sch->handle,
> + };
> struct mqprio_sched *priv = qdisc_priv(sch);
> struct net_device *dev = qdisc_dev(sch);
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131014.2639581-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [v10,net-next,2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
2026-08-31 13:10 ` [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-09-01 13:11 ` sashiko-bot
@ 2026-09-03 19:15 ` netdev-bot+sashiko
1 sibling, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:15 UTC (permalink / raw)
To: rkannoth
Cc: bpf, linux-kernel, netdev, andrew+netdev, ast, daniel, davem,
edumazet, hawk, horms, jhs, jiri, john.fastabend, kuba, pabeni,
sdf, sgoutham
Thank you for your contribution! Sashiko AI review found 11 potential
issue(s) to consider:
Critical: 0 · High: 4 · Medium: 6 · Low: 1
- [High] Stale scheduler bookkeeping makes the new liveness guard
`otx2_mqprio_mdq_allocated()` (otx2_tc.c:1710) useless:…
- [High] The stated rollback is incomplete: `otx2_mqprio_restore_old()`
(otx2_tc.c:1715-1772) restores the per-queue rate cache,…
- [High] Heap out-of-bounds write in `otx2_mqprio_restore_old()`: it
indexes the freshly allocated `pfvf->mqprio.min_rate[]`/`max_rate[]`…
- [High] The error-path rollback in `otx2_setup_tc_mqprio()` never
executes for an initial (non-replacing) mqprio add: `cleanup:` calls…
- [Medium] Undisclosed user-visible impact of the new failure path:
`otx2_mqprio_restart_netdev()` sets `OTX2_FLAG_INTF_DOWN` and calls…
- [Medium] Inconsistent netdev TC state after a failed `tc qdisc
replace`: the `cleanup:` path calls `otx2_mqprio_restore_old()`, which…
- [Medium] Teardown asymmetry in `otx2_teardown_tc_mqprio()`: in the
`if_up` branch the return value of `otx2_mqprio_down()` is saved but…
- [Medium] The interface is reopened and its TX queues are made runnable
before the new mqprio shapers and TC mapping are programmed:…
- [Medium] `otx2_mqprio_up()` programs MDQ CIR/PIR one TX queue at a time
(one mailbox transaction per queue, each arming the shaper with the…
- [Medium] Memory allocation failure during qdisc deletion aborts
teardown, leaking active hardware shapers.
- [Low] Undocumented change to the AF driver: a new
`dev_err_ratelimited()` is added to `is_valid_txschq()` in…
--
octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
This adds TC_SETUP_QDISC_MQPRIO handling for channel-mode mqprio with
TC_MQPRIO_SHAPER_BW_RATE, shaping each non-QoS TX queue through MDQ
CIR/PIR and restarting the netdev to reprogram the TX scheduler
hierarchy. Committed mqprio state is tracked in mq_offload_snap
snapshots, with rollback from the previous snapshot when a replace fails.
A few questions about the new state machine and the scheduler bookkeeping
it relies on.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> index 153eb57bad067..c9a4326d725d8 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> @@ -331,8 +331,12 @@ static bool is_valid_txschq(struct rvu *rvu, int blkaddr,
> return true;
> }
>
> - if (map_func != pcifunc)
> + if (map_func != pcifunc) {
> + dev_err_ratelimited(rvu->dev,
> + "pcifunc %x map pcifunc %x not equal, lvl=%u schq=%u\n",
> + pcifunc, map_func, lvl, schq);
> return false;
> + }
>
> return true;
> }
[Severity: Low]
The changelog describes only PF-side mqprio bandwidth offload and does not
mention the AF driver at all. This hunk changes is_valid_txschq() so that
every ownership mismatch on the mailbox path now emits a host log line,
reached from rvu_mbox_handler_nix_txschq_cfg() -> is_txschq_config_valid()
-> is_valid_txschq(). Any PF or VF, including guest VFs, can produce these
lines by asking to program a scheduler queue it does not own.
Would it make sense to split this into its own patch, or at least describe
it in the changelog?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
> index 175992188c18a..5c502c9d7c83f 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.c
> @@ -615,6 +615,142 @@ void otx2_get_mac_from_af(struct net_device *netdev)
> }
> EXPORT_SYMBOL(otx2_get_mac_from_af);
>
[ ... ]
> +int otx2_nix_tm_clear_queue_shaper(struct otx2_nic *pfvf)
> +{
> + u64 reg_addr[MAX_REGS_PER_MBOX_MSG];
> + u64 reg_val[MAX_REGS_PER_MBOX_MSG];
> + int err, smq, i, cnt = 0;
> +
> + for (i = 0; i < pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_SMQ]; i++) {
> + smq = pfvf->hw.txschq_list[NIX_TXSCH_LVL_SMQ][i];
> +
> + reg_addr[cnt] = NIX_AF_MDQX_PIR(smq);
> + reg_val[cnt] = 0;
> + cnt++;
> +
> + reg_addr[cnt] = NIX_AF_MDQX_CIR(smq);
> + reg_val[cnt] = 0;
> + cnt++;
[ ... ]
> @@ -779,6 +919,9 @@ int otx2_txsch_alloc(struct otx2_nic *pfvf)
> req->schq[NIX_TXSCH_LVL_TL4] = chan_cnt;
> }
>
> + if (pfvf->mqprio.rate_limit)
> + req->schq[NIX_TXSCH_LVL_SMQ] = pfvf->hw.non_qos_queues;
> +
> rc = otx2_sync_mbox_msg(&pfvf->mbox);
> if (rc)
> return rc;
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> index c995f29008590..79a82e4454546 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> @@ -1980,6 +1980,12 @@ int otx2_open(struct net_device *netdev)
> if (err)
> goto err_free_mem;
>
> + err = otx2_mqprio_up(pf);
> + if (err)
> + netdev_err(pf->netdev,
> + "mqprio: failed to restore shapers during open: %d; continuing without bandwidth limits\n",
> + err);
> +
[Severity: Medium]
otx2_mqprio_up() programs MDQ CIR/PIR one queue per mailbox transaction and
returns on the first failure:
err = otx2_nix_tm_set_queue_shaper(pfvf, txq, min_rate,
max_rate);
if (err) {
netdev_err(netdev,
"mqprio: failed to restore shaper for txq %d: %d\n",
txq, err);
return err;
}
Queues 0..k-1 are then left shaped and the rest run at line rate, with no
rollback, while this caller only logs "continuing without bandwidth limits"
and proceeds with pf->mqprio.rate_limit still true and the TC layout still
advertised (qopt->hw was already reported as TC_MQPRIO_HW_OFFLOAD_TCS).
Should the partially applied shapers be cleared here, so the driver's view
and the device's view do not stay divergent until the next explicit mqprio
operation?
> /* Register NAPI handler */
> for (qidx = 0; qidx < pf->hw.cint_cnt; qidx++) {
> cq_poll = &qset->napi[qidx];
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> index 039fd47ebf522..3f835eb977d3b 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> @@ -1600,14 +1609,700 @@ static int otx2_setup_tc_block(struct net_device *netdev,
> nic, nic, ingress);
> }
>
[ ... ]
> +static bool otx2_mqprio_mdq_allocated(struct otx2_nic *pfvf)
> +{
> + return pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_MDQ] != 0;
> +}
[Severity: High]
Does this predicate really tell whether the scheduler nodes are still
owned? otx2_txschq_stop() frees the TL/SMQ nodes and zeroes
hw.txschq_list[][], but leaves hw.txschq_cnt[] untouched:
otx2_common.c:otx2_txschq_stop() {
...
/* Clear the txschq list */
for (lvl = 0; lvl < NIX_TXSCH_LVL_CNT; lvl++) {
for (schq = 0; schq < MAX_TXSCHQ_PER_FUNC; schq++)
pfvf->hw.txschq_list[lvl][schq] = 0;
}
...
}
hw.txschq_cnt[] is only ever assigned in otx2_txsch_alloc(), so after
ndo_stop() this still reports MDQ allocated.
otx2_nix_tm_clear_queue_shaper() then iterates txschq_cnt[SMQ] entries of
the zeroed list and emits NIX_AF_MDQX_PIR(0)/CIR(0) writes;
otx2_get_smq_idx() returns 0 for the same reason, so otx2_mqprio_up()
programs the cached rates onto MDQ index 0.
One way in is ip link set dev X down followed by tc qdisc del dev X root:
otx2_teardown_tc_mqprio() calls otx2_mqprio_restore_old(), which has no
netif_running() guard (unlike otx2_mqprio_down()). The AF then rejects the
write in is_valid_txschq() - the same mismatch the new
dev_err_ratelimited() above logs - and that mbox error makes
otx2_mqprio_restore_old() return before otx2_mqprio_clear_sw() ever runs.
mqprio_disable_offload() discards the driver return value, so the qdisc
disappears while pfvf->mqprio.rate_limit stays true, which keeps
ethtool -L, XDP attach and PFC rejected and makes every later otx2_open()
re-widen the SMQ allocation and re-apply the stale rates.
Note also the comment on otx2_mqprio_down() saying the guard avoids
"sending shaper clears to freed queues" - is that guarantee actually
provided? Should the check look at hw.txschq_list[], or should
otx2_txschq_stop() clear txschq_cnt[]?
> +
> +static int otx2_mqprio_restore_old(struct otx2_nic *pfvf)
> +{
> + struct mq_offload_snap *snap = pfvf->old_mq_snap;
> + struct net_device *netdev = pfvf->netdev;
> + u16 num_txq = pfvf->hw.non_qos_queues;
> + int tc, txq, err;
> +
> + if (!snap)
> + return 0;
> +
> + err = otx2_mqprio_alloc_cache(pfvf, false);
> + if (err)
> + return err;
> +
> + memset(pfvf->mqprio.min_rate, 0, num_txq * sizeof(*pfvf->mqprio.min_rate));
> + memset(pfvf->mqprio.max_rate, 0, num_txq * sizeof(*pfvf->mqprio.max_rate));
> + pfvf->mqprio.flags = 0;
> +
> + for (tc = 0; tc < snap->num_tc; tc++) {
> + u64 min_rate = snap->min_rate[tc];
> + u64 max_rate = snap->max_rate[tc];
> +
> + if (min_rate)
> + pfvf->mqprio.flags |= TC_MQPRIO_F_MIN_RATE;
> + if (max_rate)
> + pfvf->mqprio.flags |= TC_MQPRIO_F_MAX_RATE;
> +
> + for (txq = snap->offset[tc];
> + txq < snap->offset[tc] + snap->count[tc]; txq++) {
> + pfvf->mqprio.min_rate[txq] = min_rate;
> + pfvf->mqprio.max_rate[txq] = max_rate;
> + }
> + }
[Severity: High]
Can this write past pfvf->mqprio.min_rate[] and max_rate[]? Those arrays
are sized by the current hw.non_qos_queues in otx2_mqprio_alloc_cache(),
while snap->offset[]/count[] were validated against a possibly larger queue
count at setup time. otx2_mqprio_validate_txqs() only runs on the setup
path, so there is no bounds re-validation here.
A sequence that reaches it:
1. mqprio bandwidth offload configured with 8 TX queues, committed as
old_mq_snap (offsets up to 8)
2. tc qdisc replace whose ndo_open() fails inside
otx2_mqprio_restart_netdev(); that path calls otx2_mqprio_clear_sw()
(clearing rate_limit) and closes the interface, then cleanup: calls
otx2_mqprio_restore_old(), which sets mqprio.handle again
3. ethtool -L dev X tx 2 is now accepted, because the new guard in
otx2_set_channels() only refuses while mqprio.rate_limit is true
4. ip link set up runs otx2_alloc_queue_mem(), which recomputes
hw.non_qos_queues = tx_queues + xdp_queues = 2
5. tc qdisc del dev X root of the still installed old qdisc matches
mqprio.handle, so otx2_mqprio_restore_old() allocates two-entry arrays
and writes indices 0..7
That would be 48 bytes past each 16 byte devm_kcalloc() buffer. Should the
restore path clamp txq against the current hw.non_qos_queues?
> +
> + pfvf->mqprio.handle = snap->handle;
[Severity: High]
Does the rollback restore a configuration that is actually active?
otx2_mqprio_restore_old() restores the rate cache, mqprio.handle and the
netdev TC mapping, but never restores pfvf->mqprio.rate_limit. When the
replace failed inside otx2_mqprio_restart_netdev(), its ndo_open() error
path already ran otx2_mqprio_clear_sw(), which cleared rate_limit.
rate_limit is the only thing that widens the SMQ allocation:
otx2_common.c:otx2_txsch_alloc() {
if (pfvf->mqprio.rate_limit)
req->schq[NIX_TXSCH_LVL_SMQ] = pfvf->hw.non_qos_queues;
}
and the only thing that parents the MDQs under TL4[0] in
otx2_txschq_config(); otx2_mqprio_up() also returns immediately when it is
false. So the restored configuration is inert (single SMQ, all TX queues
collapsed onto it by otx2_get_smq_idx(), no shapers) while the driver logs
"mqprio: replace failed; prior configuration restored".
A later tc qdisc del of the surviving qdisc then matches the restored
mqprio.handle, runs otx2_mqprio_restore_old() again and hits
if (!had_mqprio) return 0; in otx2_teardown_tc_mqprio() because had_mqprio
is rate_limit, so otx2_mqprio_clear_sw() never runs. The netdev keeps a
non-zero num_tc and tc_to_txq[] mapping with no mqprio qdisc installed;
mqprio_destroy() also skips netdev_set_num_tc(dev, 0) because the old
qdisc has hw_offload set.
> +
> + netdev_set_num_tc(netdev, snap->num_tc);
> + for (tc = 0; tc < snap->num_tc; tc++)
> + netdev_set_tc_queue(netdev, tc, snap->count[tc],
> + snap->offset[tc]);
> +
> + if (otx2_mqprio_mdq_allocated(pfvf)) {
> + err = otx2_nix_tm_clear_queue_shaper(pfvf);
> + if (err)
> + return err;
> + }
> +
> + err = otx2_mqprio_up(pfvf);
> + if (err)
> + return err;
> +
> + otx2_mqprio_snap_free(pfvf, &pfvf->cur_mq_snap);
> +
> + return 0;
> +}
[ ... ]
> +static int otx2_mqprio_restart_netdev(struct net_device *netdev, bool rate_limit)
> +{
> + struct otx2_nic *pfvf = netdev_priv(netdev);
> + const struct net_device_ops *ops = netdev->netdev_ops;
> + int err;
> +
[ ... ]
> + err = ops->ndo_stop(netdev);
> + if (err)
> + return err;
> +
> + /* Set before ndo_open() so otx2_txsch_alloc() widens SMQ allocation. */
> + if (rate_limit)
> + pfvf->mqprio.rate_limit = true;
> +
> + err = ops->ndo_open(netdev);
> + if (err) {
> + netdev_err(netdev,
> + "Failed to restart device after mqprio change: %d\n",
> + err);
[ ... ]
> + otx2_mqprio_clear_sw(pfvf);
[ ... ]
> + pfvf->flags |= OTX2_FLAG_INTF_DOWN;
> + /* visible to otx2_stop() on other cpus */
> + smp_wmb();
> + netif_close(netdev);
> + }
> +
> + return err;
> +}
[Severity: Medium]
Should the changelog mention that a failed tc qdisc add or replace can
leave the interface administratively down? It only says the hierarchy is
reprogrammed "through ndo_stop() and ndo_open() on every mqprio add,
replace, or delete", which reads as a traffic hiccup, while this path sets
OTX2_FLAG_INTF_DOWN and calls netif_close(), so recovery needs an explicit
ip link set up.
The reopen now asks the AF for hw.non_qos_queues SMQs instead of one, so
ndo_open() failure is considerably more likely than before when the shared
scheduler pool is short.
[ ... ]
> +static int otx2_teardown_tc_mqprio(struct otx2_nic *pfvf,
> + struct tc_mqprio_qopt_offload *mqprio)
> +{
> + bool had_mqprio = pfvf->mqprio.rate_limit;
> + struct tc_mqprio_qopt *qopt = &mqprio->qopt;
> + struct net_device *netdev = pfvf->netdev;
> + bool if_up = netif_running(netdev);
> + bool rollback;
> + int err;
> +
> + qopt->hw = 0;
> +
> + /* tc qdisc replace runs setup on the new mqprio before destroying the
> + * old one. Ignore teardown from the replaced instance once the new
> + * handle is committed in otx2_setup_tc_mqprio().
> + */
> + if (mqprio->handle && mqprio->handle != pfvf->mqprio.handle) {
> + if (pfvf->cur_mq_snap)
> + otx2_mqprio_snap_commit(pfvf);
> + return 0;
> + }
[Severity: High]
Does this early return turn the cleanup: path of otx2_setup_tc_mqprio()
into a no-op for an initial, non replacing mqprio add?
pfvf->mqprio.handle is assigned only after every goto cleanup site, and
mqprio->handle is sch->handle, which is set before ops->init runs:
net/sched/sch_api.c:qdisc_create() {
...
sch->handle = handle;
...
if (ops->init) {
err = ops->init(sch, tca[TCA_OPTIONS], extack);
...
}
The handle is auto allocated by qdisc_alloc_handle() when the user omits
it, so it is always non-zero and always differs from pfvf->mqprio.handle on
the failure path. The teardown then merely commits a snapshot that was
never applied as old_mq_snap and returns 0.
Failures that reach cleanup: after otx2_mqprio_restart_netdev() already
succeeded include a devm_kcalloc() -ENOMEM in otx2_mqprio_alloc_cache() and
any mbox error from otx2_nix_tm_clear_queue_shaper() or
otx2_nix_tm_set_queue_shaper(). In that case qdisc creation fails and
mqprio_destroy() resets the netdev TC count (hw_offload is 0), but the
driver keeps mqprio.rate_limit true with the widened SMQ/TL4[0] hierarchy
and partially programmed MDQ shapers, with no qdisc present.
> +
> + if (mqprio->handle && mqprio->handle == pfvf->mqprio.handle &&
> + pfvf->old_mq_snap) {
> + rollback = pfvf->cur_mq_snap;
> +
> + err = otx2_mqprio_restore_old(pfvf);
> + if (err)
> + return err;
> + if (rollback)
> + return 0;
> + }
> +
> + /* Skip the netdev restart when mqprio offload was not active. */
> + if (!had_mqprio)
> + return 0;
> +
> + if (if_up) {
> + int down_err, err;
> +
> + down_err = otx2_mqprio_down(pfvf);
> + err = otx2_mqprio_restart_netdev(netdev, false);
> + if (err)
> + return err;
> + return down_err;
> + }
[Severity: Medium]
On a plain tc qdisc del of a committed offload this branch is taken
too: mqprio->handle matches pfvf->mqprio.handle and old_mq_snap is
set, while cur_mq_snap is NULL, so rollback is false and the intent is
to fall through to the real teardown below. But
otx2_mqprio_restore_old() is still executed on the way there, and the
first thing it does is otx2_mqprio_alloc_cache(pfvf, false), which
frees the existing min_rate/max_rate arrays and re-allocates them with
devm_kcalloc().
So an -ENOMEM in a path that is only re-deriving state we are about to
throw away aborts the delete: we return here before
otx2_mqprio_down(), otx2_mqprio_restart_netdev() and
otx2_mqprio_clear_sw() run. The MDQ CIR/PIR values stay programmed
and pfvf->mqprio.rate_limit stays true, but the qdisc is destroyed
anyway because mqprio_disable_offload() has a void return and never
sees the error. Worse, alloc_cache()'s failure path calls
otx2_mqprio_free_cache(), so mqprio.min_rate/max_rate are now NULL:
the interface keeps rate_limit set - refusing ethtool -L, XDP attach
and PFC, and re-widening the SMQ allocation on every otx2_open() -
while the cached rates that otx2_mqprio_up() would need are gone and
the hardware limits linger with no qdisc to explain them.
Should the delete path skip otx2_mqprio_restore_old() entirely when
cur_mq_snap is NULL, since there is no replace to roll back? Or
should teardown log the restore failure and continue to
otx2_mqprio_down() regardless, so a deletion requested by user space
always ends with the hardware and software state cleared?
[Severity: Medium]
When otx2_mqprio_down() fails here, is restarting the interface anyway the
intended behaviour? On an mbox error otx2_mqprio_down() returns before
otx2_mqprio_clear_sw(), so rate_limit, mqprio.handle and the rate cache all
survive, and otx2_mqprio_restart_netdev(netdev, false) never clears
rate_limit - it only ever sets it:
if (rate_limit)
pfvf->mqprio.rate_limit = true;
So the reopen re-allocates one SMQ per queue and otx2_open()'s
otx2_mqprio_up() re-programs every cached shaper for a qdisc that is being
destroyed. mqprio_disable_offload() has a void return, so user space never
learns about it.
> +
> + /* ndo_stop() already freed the TX scheduler TL nodes; drop software
> + * state only.
> + */
> + otx2_mqprio_clear_sw(pfvf);
> + return 0;
> +}
> +
> +static int otx2_setup_tc_mqprio(struct net_device *netdev,
> + struct tc_mqprio_qopt_offload *mqprio)
> +{
[ ... ]
> + err = otx2_mqprio_stage_cur(pfvf, mqprio);
> + if (err)
> + return err;
> +
> + err = otx2_mqprio_restart_netdev(pfvf->netdev, true);
> + if (err)
> + goto cleanup;
> +
> + err = otx2_mqprio_alloc_cache(pfvf, replacing);
> + if (err)
> + goto cleanup;
> +
> + /* otx2_mqprio_up() may have restored the previous configuration during
> + * the restart above. Clear every MDQ shaper before applying the new
> + * mapping so queues dropped from the TC layout do not keep stale
> + * limits in hardware.
> + */
> + if (otx2_mqprio_mdq_allocated(pfvf)) {
> + err = otx2_nix_tm_clear_queue_shaper(pfvf);
> + if (err)
> + goto cleanup;
> + }
[Severity: Medium]
Is there a window here where traffic egresses with no shapers, or with a
half applied TC map? otx2_mqprio_restart_netdev() has already reopened the
device, and on an already up link otx2_open() reaches
otx2_handle_link_event() -> netif_tx_start_all_queues(), so TX is running
again before this code clears every MDQ shaper, programs the queues one
mailbox at a time, and finally publishes netdev_set_num_tc() followed by
the individual netdev_set_tc_queue() entries.
RTNL does not serialize against the transmit path, and for a newly created
qdisc tc_modify_qdisc() calls qdisc_create() (and therefore this callback)
before qdisc_graft() does dev_deactivate(), so the core does not quiesce
the datapath either. A transmitting CPU can see num_tc published before
the matching tc_to_txq[] entry is set.
> +
> + pfvf->mqprio.flags = mqprio->flags;
> +
[ ... ]
> + netdev_set_num_tc(netdev, pfvf->cur_mq_snap->num_tc);
> + for (i = 0; i < pfvf->cur_mq_snap->num_tc; i++)
> + netdev_set_tc_queue(netdev, i, pfvf->cur_mq_snap->count[i],
> + qopt->offset[i]);
> +
> + qopt->hw = TC_MQPRIO_HW_OFFLOAD_TCS;
> + pfvf->mqprio.handle = mqprio->handle;
> +
> + if (!replacing)
> + otx2_mqprio_snap_commit(pfvf);
> +
> + return 0;
> +
> +cleanup:
> + qopt->hw = 0;
> + if (replacing) {
> + int restore_err = otx2_mqprio_restore_old(pfvf);
> +
[ ... ]
> + } else {
> + netdev_err(netdev,
> + "mqprio: replace failed; prior configuration restored\n");
> + if (extack)
> + NL_SET_ERR_MSG_MOD(extack,
> + "mqprio: replace failed; prior configuration restored");
> + }
> + return err ? err : -EIO;
> + }
[Severity: Medium]
otx2_mqprio_restore_old() re-applies netdev_set_num_tc() and
netdev_set_tc_queue(), but this path then returns an error to the core.
mqprio_enable_offload() propagates it without setting priv->hw_offload, so
mqprio_init() fails and qdisc_create() runs:
net/sched/sch_api.c:qdisc_create() {
err_out4:
/* Even if ops->init() failed, we call ops->destroy() */
if (ops->destroy)
ops->destroy(sch);
...
}
and mqprio_destroy() takes the non offloaded branch:
if (priv->hw_offload && dev->netdev_ops->ndo_setup_tc)
mqprio_disable_offload(sch);
else
netdev_set_num_tc(dev, 0);
Does that wipe the mapping the driver just restored, even though the old
mqprio qdisc is still grafted with hw_offload set? The end state has the
driver keeping rate_limit, the cached rates, the programmed MDQ CIR/PIR and
the TL4 flattened hierarchy, while the netdev has no TC mapping at all, so
queue selection no longer follows the surviving qdisc's layout.
> + otx2_teardown_tc_mqprio(pfvf, mqprio);
> + return err;
> +}
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131014.2639581-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-03 19:15 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 13:10 [PATCH v10 net-next 0/2] octeontx2-pf: mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-08-31 13:10 ` [PATCH v10 net-next 1/2] net/sched: mqprio: pass qdisc handle to offload drivers Ratheesh Kannoth
2026-09-03 19:15 ` [v10,net-next,1/2] " netdev-bot+sashiko
2026-08-31 13:10 ` [PATCH v10 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-09-01 13:11 ` sashiko-bot
2026-09-03 19:15 ` [v10,net-next,2/2] " netdev-bot+sashiko
2026-09-02 1:39 ` [PATCH v10 net-next 0/2] octeontx2-pf: " Ratheesh Kannoth
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox