|WARNING| eal: do not dispatch newly registered callbacks
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Tue Nov 3 13:55:34 CET 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/172370
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-11-03
# DPDK Patch Review
## ERRORS
**Error: Use-after-free on callback iteration**
The iteration pattern has a critical use-after-free bug. When a callback invokes `rte_intr_callback_unregister()` on itself (which net/tap does, per the commit message), the `cb` pointer becomes invalid. The next iteration then accesses freed memory in the `if (cb == last)` check.
```c
for (cb = TAILQ_FIRST(&src->callbacks); cb != NULL; cb = next) {
next = TAILQ_NEXT(cb, next);
/* cb copy, unlock, invoke callback */
rte_spinlock_lock(&intr_lock);
if (cb == last) /* BUG: cb may have been freed by the callback */
break;
}
```
The callback may unregister itself while the lock is dropped. When control returns, `cb` is a dangling pointer. Dereferencing it for the comparison is undefined behavior.
**Suggested fix**: Track the stop condition before entering the loop, using `last`'s `next` pointer or a detached sentinel. Since `last` itself may also be unregistered, the safest approach is to iterate a fixed count:
```c
/* Count callbacks registered before dispatch */
int num_to_call = 0;
TAILQ_FOREACH(cb, &src->callbacks, next)
num_to_call++;
/* Invoke only that many */
cb = TAILQ_FIRST(&src->callbacks);
while (num_to_call > 0 && cb != NULL) {
next = TAILQ_NEXT(cb, next);
active_cb = *cb;
rte_spinlock_unlock(&intr_lock);
active_cb.cb_fn(active_cb.cb_arg);
rte_spinlock_lock(&intr_lock);
num_to_call--;
cb = next;
}
```
This avoids all pointer dereferences of potentially-freed callback nodes.
---
**Error: Missing null check after callback invocation**
After dropping and re-acquiring the lock, `next` (computed before the unlock) may point to a callback that has been unregistered and freed. The code does not verify `next` is still valid before using it in the next iteration.
If the callback unregisters the *next* callback in the chain, `next` becomes a dangling pointer. The loop then accesses freed memory on the next iteration.
**Suggested fix**: The count-based approach above also solves this, since it does not rely on chasing freed pointers.
---
## WARNINGS
**Warning: `last` pointer may be stale**
The patch reads `last = TAILQ_LAST(&src->callbacks, ...)` before dropping the lock, then compares `cb == last` after callbacks have run. If a callback appends a new callback (the very scenario the patch aims to handle), or if `last` itself is unregistered, the pointer comparison may be meaningless or unsafe.
**Suggested fix**: The count-based iteration in the Error fix above avoids this issue entirely.
---
Review-Result: ERROR
More information about the test-report
mailing list