|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