|WARNING| [v2,2/2] net/mlx5: move to polling for LSC interrupts
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Fri Oct 30 19:05:30 CET 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/172320
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-10-30
# DPDK Patch Review
## PATCH 1/2: net/mlx5: fix link status query
### Errors
None.
### Warnings
None.
### Info
None.
---
## PATCH 2/2: net/mlx5: move to polling for LSC interrupts
### Errors
**Error: Resource leak on error path**
In `mlx5_lsc_intr_cb()` at `drivers/net/mlx5/linux/mlx5_lsc_intr.c:249`:
```c
ret = rte_eal_alarm_set(delay_us, mlx5_lsc_intr_cb, ctx);
if (ret < 0) {
DRV_LOG(ERR, "rescheduling link polling callback failed (%s)",
rte_strerror(-ret));
lsc_intr_disarm(ctx);
}
```
If `rte_eal_alarm_set()` fails, the function logs an error and disarms the context, but the callback returns without cleaning up the devices that were added via `lsc_intr_dev_add()`. The devices remain in `ctx->polled_ports`, causing subsequent callbacks to attempt to poll devices that may be in an inconsistent state.
**Suggested fix:**
```c
if (ret < 0) {
DRV_LOG(ERR, "rescheduling link polling callback failed (%s)",
rte_strerror(-ret));
/* Clear all polled ports since we cannot reschedule */
rte_spinlock_lock(&ctx->lock);
rte_bitset_clear_all(ctx->polled_ports, POLLED_PORTS_MAX);
ctx->armed = false;
rte_spinlock_unlock(&ctx->lock);
}
```
**Error: Use-after-free risk in callback waiting loop**
In `mlx5_lsc_intr_cleanup()` at `drivers/net/mlx5/linux/mlx5_lsc_intr.c:289-291`:
```c
while (lsc_intr_cb_in_progress(ctx) && !rte_thread_is_intr())
rte_pause();
```
The device is removed from `ctx->polled_ports` before this wait loop. If `lsc_intr_cb()` has already gathered the device pointer into its local `to_process` array (line 213), the callback will access the device via `mlx5_link_carrier_get()` and `mlx5_link_update()` after cleanup has proceeded past the wait loop. This is a use-after-free: the device structure may be freed while the callback is still using it.
The race window: (1) callback gathers device pointers, (2) cleanup removes device from bitset and passes the wait loop (because callback hasn't set in_progress yet), (3) device is freed, (4) callback accesses freed device.
**Suggested fix:**
Set `cb_in_progress` before gathering device pointers, not after:
```c
/* In mlx5_lsc_intr_cb */
lsc_intr_cb_set_in_progress(ctx, true);
reschedule = lsc_intr_cb(ctx, &delay_us);
/* Remove the second set-in-progress call after lsc_intr_cb */
```
And in `lsc_intr_cb`, remove the local setting of `cb_in_progress` -- the outer function now owns it.
**Error: Missing bounds check on port_id in callback**
In `lsc_intr_gather_devs()` at `drivers/net/mlx5/linux/mlx5_lsc_intr.c:150-152`:
```c
RTE_BITSET_FOREACH_SET(port_id, ctx->polled_ports, POLLED_PORTS_MAX) {
out[ret++] = &rte_eth_devices[port_id];
}
```
If `port_id >= RTE_MAX_ETHPORTS`, the access to `rte_eth_devices[port_id]` is out of bounds. While the bitset size is `POLLED_PORTS_MAX` (which equals `RTE_MAX_ETHPORTS`), there is no guarantee that only valid port IDs were added -- a logic error elsewhere could set a bit beyond the valid range.
**Suggested fix:**
```c
RTE_BITSET_FOREACH_SET(port_id, ctx->polled_ports, POLLED_PORTS_MAX) {
if (port_id >= RTE_MAX_ETHPORTS) {
DRV_LOG(ERR, "invalid port_id %zd in polled_ports", port_id);
continue;
}
out[ret++] = &rte_eth_devices[port_id];
}
```
### Warnings
**Warning: Spinlock held across logging call**
In `adjust_period_and_calc_delay()` at `drivers/net/mlx5/linux/mlx5_lsc_intr.c:89-91`:
```c
DRV_LOG(DEBUG, "adjusting link polling period: %" PRIu64 " us to %" PRIu64 " us",
ctx->period_us, new_period);
```
This function is called while holding `ctx->lock` in the caller `lsc_intr_cb()` (via `lsc_intr_gather_devs`). Logging while holding a spinlock can cause delays or deadlock if the log backend acquires locks.
**Suggested fix:** Move the logging outside the locked region:
```c
/* In lsc_intr_cb, after lsc_intr_gather_devs returns */
if (new_period != ctx->period_us)
DRV_LOG(DEBUG, "adjusting link polling period: %" PRIu64 " us to %" PRIu64 " us",
old_period, new_period);
```
And pass `old_period` and `new_period` out of `adjust_period_and_calc_delay`.
**Warning: No bounds check on to_process array**
In `lsc_intr_gather_devs()` at `drivers/net/mlx5/linux/mlx5_lsc_intr.c:143-156`:
```c
static int
lsc_intr_gather_devs(struct mlx5_lsc_intr_ctx *ctx,
struct rte_eth_dev *out[POLLED_PORTS_MAX])
{
ssize_t port_id;
int ret = 0;
rte_spinlock_lock(&ctx->lock);
RTE_BITSET_FOREACH_SET(port_id, ctx->polled_ports, POLLED_PORTS_MAX) {
out[ret++] = &rte_eth_devices[port_id];
}
rte_spinlock_unlock(&ctx->lock);
return ret;
}
```
If more than `POLLED_PORTS_MAX` bits are set in `ctx->polled_ports` (which cannot happen given the bitset size, but the code does not assert this), `ret` could overflow the `out` array. While the bitset API prevents this in practice, defensive programming would add an assertion.
**Suggested fix:**
```c
RTE_BITSET_FOREACH_SET(port_id, ctx->polled_ports, POLLED_PORTS_MAX) {
MLX5_ASSERT(ret < POLLED_PORTS_MAX);
out[ret++] = &rte_eth_devices[port_id];
}
```
**Warning: Potential division by zero**
In `adjust_period_and_calc_delay()` at `drivers/net/mlx5/linux/mlx5_lsc_intr.c:78-85`:
```c
if (delta_us > new_period / 2) {
/* If takes too long, adjust period up. */
while (new_period < LINK_POLL_MAXIMUM_PERIOD_US && delta_us > new_period / 2)
new_period = RTE_MIN(new_period * 2, LINK_POLL_MAXIMUM_PERIOD_US);
} else if (delta_us < new_period / 4) {
/* If polling is fast, adjust period down. */
while (new_period > LINK_POLL_MINIMUM_PERIOD_US && delta_us < new_period / 4)
new_period = RTE_MAX(new_period / 2, LINK_POLL_MINIMUM_PERIOD_US);
}
```
If `new_period` is zero (due to corruption or logic error), the divisions `/2` and `/4` would cause division by zero. While `new_period` is initialized to `ctx->period_us` (which is initialized to `LINK_POLL_DEFAULT_PERIOD_US`), defensive code should validate it.
**Suggested fix:**
```c
new_period = ctx->period_us;
if (new_period == 0) {
DRV_LOG(ERR, "link polling period is zero, resetting to default");
new_period = LINK_POLL_DEFAULT_PERIOD_US;
}
```
### Info
**Pre-existing: mlx5_link_update return value semantics inverted**
In patch 1, at `drivers/net/mlx5/linux/mlx5_ethdev_os.c:578`:
```c
return rte_eth_linkstatus_set(dev, &dev_link) == 0;
```
`rte_eth_linkstatus_set()` returns -1 when the link status changed, 0 when it did not change. The new code inverts this: `== 0` becomes `true` (1), meaning "no change", but the caller expects the opposite (return 1 when link changed). This makes the callback in patch 2 suppress notifications when the link actually changed.
Note: This issue is in patch 1, which the current review scope includes. However, it is a logic error, not a style or process issue, and would cause incorrect behaviour.
**Suggested fix:**
```c
return rte_eth_linkstatus_set(dev, &dev_link) == -1;
```
Or more idiomatically:
```c
return rte_eth_linkstatus_set(dev, &dev_link) != 0;
```
---
Review-Result: ERROR
More information about the test-report
mailing list