BPF List
 help / color / mirror / Atom feed
From: netdev-bot+sashiko@kernel.org
To: rkannoth@marvell.com
Cc: bpf@vger.kernel.org, linux-kernel@vger.kernel.org,
	netdev@vger.kernel.org, andrew+netdev@lunn.ch, ast@kernel.org,
	daniel@iogearbox.net, davem@davemloft.net, edumazet@google.com,
	hawk@kernel.org, horms@kernel.org, jhs@mojatatu.com,
	jiri@resnulli.us, john.fastabend@gmail.com, kuba@kernel.org,
	pabeni@redhat.com, sdf@fomichev.me, sgoutham@marvell.com
Subject: Re: [v10,net-next,2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
Date: Thu, 03 Sep 2026 19:15:01 +0000	[thread overview]
Message-ID: <178846290196.3394541.5288115406015228702@kernel.org> (raw)
In-Reply-To: <20260831131014.2639581-3-rkannoth@marvell.com>

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

  parent reply	other threads:[~2026-09-03 19:15 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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   ` netdev-bot+sashiko [this message]
2026-09-02  1:39 ` [PATCH v10 net-next 0/2] octeontx2-pf: " 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=178846290196.3394541.5288115406015228702@kernel.org \
    --to=netdev-bot+sashiko@kernel.org \
    --cc=andrew+netdev@lunn.ch \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=hawk@kernel.org \
    --cc=horms@kernel.org \
    --cc=jhs@mojatatu.com \
    --cc=jiri@resnulli.us \
    --cc=john.fastabend@gmail.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=rkannoth@marvell.com \
    --cc=sdf@fomichev.me \
    --cc=sgoutham@marvell.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox