From: sashiko-bot@kernel.org
To: "Ratheesh Kannoth" <rkannoth@marvell.com>, bpf@vger.kernel.org
Subject: Re: [PATCH v21 net-next 2/2] octeontx2: add mqprio bandwidth offload for NIX TX schedulers
Date: Tue, 10 Nov 2026 03:33:11 +0000 [thread overview]
Message-ID: <sashiko-outbox-206123@kernel.org> (raw)
In-Reply-To: <20261109033154.2161803-3-rkannoth@marvell.com>
Thank you for your contribution! Sashiko AI review found 7 potential
issues to consider:
- [Medium] octeontx2: otx2_teardown_tc_mqprio() commits aborted replace
without graft
- [Medium] octeontx2: skipped netdev restart on shaper clear error in
otx2_teardown_tc_mqprio()
- [Medium] octeontx2-pf: premature otx2_mcam_flow_del() in otx2_probe()
error path
- [Medium] octeontx2: otx2_mqprio_clear_sw() wipes netdev TC layout on
taprio replace
- [Medium] octeontx2: incomplete rollback and state loss in
otx2_mqprio_restore_old()
- [Medium] octeontx2: broken mqprio replace rollback in
otx2_mqprio_restart_netdev()
- [Medium] octeontx2: lost netdev TC state on rejected replace in
otx2_setup_tc_mqprio()
--
commit 9864a43cee348b7e8d668cd98a7aae2752988212
Author: Ratheesh Kannoth <rkannoth@marvell.com>
octeontx2: add mqprio bandwidth offload for NIX TX schedulers
This commit adds TC_SETUP_QDISC_MQPRIO bandwidth offload support for PF
and VF netdevs by reprogramming NIX TX scheduler MDQ CIR/PIR registers
and bouncing the netdev to allocate one SMQ per non-QoS queue.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> index 777e7156badb7..f51161e8b5949 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
[ ... ]
> @@ -3420,11 +3437,13 @@ static int otx2_probe(struct pci_dev *pdev, const struct pci_device_id *id)
> otx2_sriov_vfcfg_cleanup(pf);
> err_pf_sriov_init:
> otx2_unregister_dl(pf);
> - otx2_shutdown_tc(pf);
> err_mcam_flow_del:
> otx2_mcam_flow_del(pf);
> err_unreg_netdev:
> + otx2_shutdown_tc_mqprio(pf);
> unregister_netdev(netdev);
> +err_shutdown_tc:
> + otx2_shutdown_tc(pf);
> err_ipsec_clean:
> cn10k_ipsec_clean(pf);
> err_mcs_free:
[Severity: Medium]
Does moving otx2_shutdown_tc(pf) below err_mcam_flow_del cause
otx2_destroy_tc_flow_list() to skip freeing flow_cfg->flow_list_tc
entries when probe fails after register_netdev()?
otx2_mcam_flow_del() calls otx2_destroy_mcam_flows(), which frees the
MCAM entries while the netdev is still registered and clears
OTX2_FLAG_MCAM_ENTRIES_ALLOC. When otx2_shutdown_tc() then calls
otx2_destroy_tc_flow_list(), it sees OTX2_FLAG_MCAM_ENTRIES_ALLOC clear
and returns early without freeing any otx2_tc_flow nodes added while the
netdev was registered:
drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c:otx2_destroy_tc_flow_list() {
...
if (!otx2_test_flag(pfvf, OTX2_FLAG_MCAM_ENTRIES_ALLOC))
return;
list_for_each_entry_safe(iter, tmp, &flow_cfg->flow_list_tc, list) {
list_del(&iter->list);
kfree(iter);
flow_cfg->nr_flows--;
}
}
err_del_mcam_entries below err_mcs_free already calls
otx2_mcam_flow_del(pf), so err_mcam_flow_del can be dropped.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> index 8877af348a090..b34a86259e786 100644
> --- 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)
> +{
> + 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) {
> + netdev_err(netdev,
> + "mqprio: rollback failed to allocate rate cache: %d\n",
> + 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 = snap->flags;
> +
> + for (tc = 0; tc < snap->num_tc; tc++) {
> + u64 min_rate = snap->min_rate[tc];
> + u64 max_rate = snap->max_rate[tc];
> +
> + 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;
> + }
> + }
> +
> + otx2_mqprio_apply_snap_netdev(netdev, snap);
> +
> + if (otx2_mqprio_mdq_allocated(pfvf)) {
> + err = otx2_nix_tm_clear_queue_shaper(pfvf);
> + if (err) {
> + netdev_err(netdev,
> + "mqprio: rollback shaper clear failed: %d; hardware limits may not match qdisc\n",
> + err);
> + return err;
> + }
> + }
> +
> + /* Rebuild the TX scheduler via netdev restart when running; otx2_mqprio_up()
> + * alone is insufficient after a failed replace that already bounced the
> + * interface. If open failed, TX schedulers were freed; defer shaper restore
> + * to the next successful ndo_open() via otx2_mqprio_up().
> + */
> + pfvf->mqprio.rate_limit = true;
> +
> + if (netif_running(netdev)) {
> + err = otx2_mqprio_restart_netdev(netdev, true);
> + if (err) {
> + netdev_err(netdev,
> + "mqprio: rollback netdev restart failed: %d; qdisc rates may not be enforced\n",
> + err);
> + return err;
> + }
> + } else if (pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_SMQ]) {
> + err = otx2_mqprio_up(pfvf);
> + if (err) {
> + netdev_err(netdev,
> + "mqprio: rollback shaper restore failed: %d; qdisc rates may not be enforced\n",
> + err);
> + return err;
> + }
> + }
> +
> + otx2_mqprio_snap_free(pfvf, &pfvf->cur_mq_snap);
> +
> + return 0;
> +}
[Severity: Medium]
Can an error in otx2_mqprio_restore_old() discard the restored rate
cache and leave the surviving mqprio qdisc out of sync with hardware?
If otx2_mqprio_restart_netdev(netdev, true) fails in ops->ndo_open(), it
calls otx2_mqprio_clear_sw(pfvf), which sets pfvf->mqprio.rate_limit to
false and frees pfvf->mqprio.min_rate and max_rate. Contrary to the
comment above, the next ndo_open() will then see rate_limit == false,
allocate only a single SMQ in otx2_txsch_alloc(), and return early in
otx2_mqprio_up(), leaving traffic unshaped while the old offloaded
mqprio qdisc remains attached.
Passing replacing = false to otx2_mqprio_alloc_cache() also forces new
allocations instead of reusing the existing arrays, and any early return
from otx2_mqprio_restore_old() skips freeing pfvf->cur_mq_snap and
causes the cleanup path in otx2_setup_tc_mqprio() to skip
otx2_mqprio_defer_netdev_tc_restore(pfvf), so mqprio_destroy() on the
rejected new qdisc resets netdev->num_tc to 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);
> +}
> +
> +/* Offloaded mqprio replaced by software mqprio installs netdev TC layout in
> + * mqprio_init() before the old offload instance is destroyed during graft.
> + */
> +static bool otx2_mqprio_keep_netdev_tc(struct otx2_nic *pfvf)
> +{
> + struct Qdisc *qdisc = rtnl_dereference(pfvf->netdev->qdisc);
> +
> + return qdisc && qdisc->ops && !strcmp(qdisc->ops->id, "mqprio");
> +}
> +
> +static void otx2_mqprio_clear_sw(struct otx2_nic *pfvf)
> +{
> + struct net_device *netdev = pfvf->netdev;
> +
> + pfvf->mqprio.rate_limit = false;
> + otx2_mqprio_clear_replace_state(pfvf);
> + if (!otx2_mqprio_keep_netdev_tc(pfvf)) {
> + struct Qdisc *root = rtnl_dereference(netdev->qdisc);
> +
> + if (root && root->ops && !strcmp(root->ops->id, "taprio"))
> + netdev_warn(netdev,
> + "mqprio: offload teardown cleared the TC layout of the new %s\n",
> + root->ops->id);
> + netdev_set_num_tc(netdev, 0);
> + }
> + otx2_mqprio_free_cache(pfvf);
> +}
[Severity: Medium]
When tc qdisc replace replaces an offloaded mqprio qdisc with a software
taprio qdisc, does calling netdev_set_num_tc(netdev, 0) here break the
new taprio qdisc?
During replacement, taprio_init() -> taprio_change() configures
netdev->num_tc, netdev->tc_to_txq, and netdev->prio_tc_map before
qdisc_graft() publishes the new taprio qdisc and destroys the old mqprio
qdisc. Resetting num_tc to 0 in otx2_mqprio_clear_sw() causes
taprio_dequeue_tc_priority() (which loops from netdev_get_num_tc(dev) - 1
down to 0) to do zero iterations and stop dequeuing TX packets.
Preserving the netdev TC layout in otx2_mqprio_keep_netdev_tc() when the
new root qdisc is taprio would avoid wiping out taprio's configuration.
[ ... ]
> +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;
> + bool running = netif_running(netdev);
> + int err;
> +
> + /* TODO: Explore live TX scheduler reprogramming to avoid a full
> + * ndo_stop()/ndo_open() bounce on every mqprio change.
> + */
> + netdev_dbg(netdev,
> + "mqprio: restarting interface to reprogram TX scheduler\n");
> +
> + if (running) {
> + clear_bit(__LINK_STATE_START, &netdev->state);
> + smp_mb__after_atomic(); /* Commit netif_running(). */
> + }
> +
> + err = ops->ndo_stop(netdev);
> + if (err) {
> + if (running)
> + set_bit(__LINK_STATE_START, &netdev->state);
> + return err;
> + }
> +
> + /* Set before ndo_open() so otx2_txsch_alloc() widens SMQ allocation.
> + * On teardown, drop mqprio software state so ndo_open() does not
> + * re-apply bandwidth limits via otx2_mqprio_up() after the kernel
> + * removed the qdisc.
> + */
> + if (rate_limit)
> + pfvf->mqprio.rate_limit = true;
> + else
> + otx2_mqprio_clear_sw(pfvf);
> +
> + err = ops->ndo_open(netdev);
> + if (!err && running) {
> + set_bit(__LINK_STATE_START, &netdev->state);
> + } else if (err) {
> + netdev_err(netdev,
> + "Failed to restart device after mqprio change: %d; bringing interface down\n",
> + err);
> + /* ndo_open() already tore down TX scheduler resources on failure;
> + * netif_running() is false here (__LINK_STATE_START stays clear
> + * until open succeeds). 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
> + * otx2_stop() returns early when netif_close() runs. Caller
> + * holds RTNL; netif_close() is used since it needs no extra
> + * locking (dev_close() would also take the netdev ops lock).
> + */
> + otx2_set_flag(pfvf, OTX2_FLAG_INTF_DOWN);
> + /* visible to otx2_stop() on other cpus */
> + smp_wmb();
> + netif_close(netdev);
> + }
> +
> + return err;
> +}
[Severity: Medium]
What happens during a tc qdisc replace if ops->ndo_open() fails in the
first otx2_mqprio_restart_netdev() call in otx2_setup_tc_mqprio()?
When ops->ndo_open() fails, otx2_free_hw_resources() ->
otx2_txschq_stop() zeroes pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_SMQ], and
this error path leaves __LINK_STATE_START clear and calls
netif_close(netdev) from inside ndo_setup_tc() (which runs
dev_deactivate_many() and emits NETDEV_GOING_DOWN/NETDEV_DOWN notifiers
mid-qdisc operation). When otx2_setup_tc_mqprio() then jumps to cleanup
and calls otx2_mqprio_restore_old(), both netif_running(netdev) and
pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_SMQ] are 0, so
otx2_mqprio_restore_old() returns 0 without restoring hardware limits,
and cleanup logs and sets extack to "mqprio: replace failed; hardware
limits restored, netdev TC layout restore deferred" even though the
interface was closed.
[ ... ]
> +static bool otx2_tc_can_offload(struct net_device *netdev)
> +{
> + return !!(netdev->hw_features & NETIF_F_HW_TC);
> +}
> +
> +static int otx2_teardown_tc_mqprio(struct otx2_nic *pfvf,
> + struct tc_mqprio_qopt_offload *mqprio)
> +{
> + struct netlink_ext_ack *extack = mqprio->extack;
> + struct tc_mqprio_qopt *qopt = &mqprio->qopt;
> + bool had_mqprio = pfvf->mqprio.rate_limit;
> + struct net_device *netdev = pfvf->netdev;
> + bool if_up = netif_running(netdev);
> + int err;
> +
> + if (!otx2_tc_can_offload(netdev))
> + return -EOPNOTSUPP;
> +
> + qopt->hw = 0;
> +
> + /* tc qdisc replace runs setup on the new mqprio before destroying the
> + * old one. replace_setup_done and TC_ROOT_GRAFT distinguish stale
> + * old-instance teardown from graft failure after setup.
> + * qdisc_offload_graft_helper() skips TC_ROOT_GRAFT unless hw-tc-offload
> + * is enabled in netdev->features; commit without graft in that case.
> + */
> + if (pfvf->mqprio.replace_setup_done && pfvf->cur_mq_snap) {
> + err = 0;
> + if (pfvf->mqprio.replace_graft_done || !tc_can_offload(netdev)) {
> + otx2_mqprio_snap_commit(pfvf);
> + } else {
> + err = otx2_mqprio_restore_old(pfvf);
> + if (err && extack)
> + NL_SET_ERR_MSG_FMT_MOD(extack,
> + "mqprio: rollback failed (%d)",
> + err);
> + }
> + otx2_mqprio_clear_replace_state(pfvf);
> + return err;
> + }
[Severity: Medium]
Does checking !tc_can_offload(netdev) here commit a rejected replacement
instead of rolling back when qdisc_create() fails after mqprio_init()?
In both otx2_probe() and otx2vf_probe(), NETIF_F_HW_TC is added to
netdev->hw_features after netdev->features |= netdev->hw_features, so
!tc_can_offload(netdev) is true by default while otx2_tc_can_offload()
still permits mqprio offload. If a replacement mqprio qdisc succeeds in
mqprio_init() -> otx2_setup_tc_mqprio() (setting replace_setup_done and
cur_mq_snap) but qdisc_create() then fails before qdisc_graft() (for
example when TCA_RATE is passed on a TCQ_F_MQROOT qdisc), err_out4 in
qdisc_create() calls mqprio_destroy() -> otx2_teardown_tc_mqprio() on
the rejected new qdisc. Because !tc_can_offload(netdev) is true, this
branch calls otx2_mqprio_snap_commit() instead of
otx2_mqprio_restore_old(), leaving the old mqprio qdisc attached while
hardware shapers, software rate caches, and old_mq_snap retain the
rejected replacement configuration.
Checking tc_can_offload(netdev) in otx2_tc_can_offload() so offload
requires NETIF_F_HW_TC in netdev->features would ensure TC_ROOT_GRAFT is
always sent on a successful graft.
> +
> + /* Skip the netdev restart when mqprio offload was not active. */
> + if (!had_mqprio)
> + return 0;
> +
> + if (if_up) {
> + err = otx2_mqprio_down(pfvf);
> + if (err) {
> + if (extack)
> + NL_SET_ERR_MSG_FMT_MOD(extack,
> + "mqprio: teardown incomplete (%d)",
> + err);
> + netdev_err(netdev,
> + "mqprio: offload teardown incomplete (%d); driver state may not match removed qdisc\n",
> + err);
> + return err;
> + }
> +
> + err = otx2_mqprio_restart_netdev(netdev, false);
[Severity: Medium]
Does returning early when otx2_mqprio_down() fails skip the netdev
restart that otx2_mqprio_down() relies on?
otx2_mqprio_down() unconditionally calls otx2_mqprio_clear_sw(pfvf)
(setting pfvf->mqprio.rate_limit to false, resetting num_tc, and freeing
the rate caches) and notes in its comment that the following netdev
restart reallocates the TX scheduler and drops any shaper left
programmed. Returning early here on a shaper-clear error skips
otx2_mqprio_restart_netdev(netdev, false), and because
mqprio_disable_offload() ignores the return value, the qdisc is
destroyed while the interface stays up with the multi-SMQ hierarchy and
stale MDQ rate limits active in hardware, and with rate_limit == false
no longer blocking PFC or HTB setup.
[ ... ]
> +static int otx2_setup_tc_mqprio(struct net_device *netdev,
> + struct tc_mqprio_qopt_offload *mqprio)
> +{
> + struct netlink_ext_ack *extack = mqprio->extack;
> + struct otx2_nic *pfvf = netdev_priv(netdev);
> + struct tc_mqprio_qopt *qopt = &mqprio->qopt;
> + bool replacing = pfvf->mqprio.rate_limit;
> + bool if_up = netif_running(netdev);
> + int tc, txq, err, i;
> +
> + if (!otx2_tc_can_offload(netdev))
> + return -EOPNOTSUPP;
> +
> + 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");
> + err = -EOPNOTSUPP;
> + goto fail_validate;
> + }
> +
> + if (!replacing && otx2_mqprio_keep_netdev_tc(pfvf)) {
> + netdev_err(netdev,
> + "mqprio: delete existing mqprio before re-enabling hw/sw offload\n");
> + NL_SET_ERR_MSG_MOD(extack,
> + "mqprio: delete existing mqprio before re-enabling hw/sw offload");
> + return -EOPNOTSUPP;
> + }
[Severity: Medium]
When rejecting a replace of an existing software mqprio qdisc (hw 0)
with a hardware-offloaded mqprio qdisc (hw 1), does the error unwind in
qdisc_create() wipe out the software mqprio qdisc's netdev TC layout?
When otx2_setup_tc_mqprio() returns -EOPNOTSUPP here (or at the !if_up
check above), qdisc_create() jumps to err_out4 and calls
mqprio_destroy() on the failed new qdisc. Because priv->hw_offload is 0
on the failed qdisc, mqprio_destroy() calls netdev_set_num_tc(dev, 0),
clearing the num_tc and queue mappings of the still-attached software
mqprio qdisc without old_mq_snap or netdev_tc_work restoring them.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109033154.2161803-1-rkannoth@marvell.com?part=2
next prev parent reply other threads:[~2026-11-10 3:33 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-09 3:31 [PATCH v21 net-next 0/2] octeontx2: mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-11-09 3:31 ` [PATCH v21 net-next 1/2] octeontx2: use atomic bitops for PF/VF and rep flags Ratheesh Kannoth
2026-11-10 3:33 ` sashiko-bot
2026-11-11 21:32 ` netdev-bot+sashiko
2026-11-12 1:56 ` Ratheesh Kannoth
2026-11-09 3:31 ` [PATCH v21 net-next 2/2] octeontx2: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-11-10 3:33 ` sashiko-bot [this message]
2026-11-11 21:32 ` netdev-bot+sashiko
2026-11-12 1:55 ` Ratheesh Kannoth
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=sashiko-outbox-206123@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=rkannoth@marvell.com \
--cc=sashiko-reviews@lists.linux.dev \
/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