|WARNING| [v5,5/5] eal: avoid deadlock in async IPC alarm callback
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Mon Jun 8 16:27:15 CEST 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/165163
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-06-08
# DPDK Patch Review
## Patch 1/5: eal: fix wrong log message in async IPC request
**No issues found.**
---
## Patch 2/5: eal: fix async IPC callback not fired when no peers
### Errors
**Error path alarm cleanup missing**
When `rte_eal_alarm_set()` fails in the secondary process path, the error path jumps to `unlock_fail` which only unlocks the mutex and goes to the common `fail` label. However, the `fail` label does not cancel the alarm that might have been set earlier for the dummy request. If the alarm was successfully set but a later error occurs, the alarm will fire with a dangling pointer to `dummy`.
The fix should cancel the alarm before freeing `dummy` on the error path:
```c
fail:
rte_eal_alarm_cancel(async_reply_handle, (void *)(uintptr_t)dummy->id);
free(dummy);
free(copy);
free(param);
return ret;
```
Note: The primary process path has the same issue at the `closedir_fail` label.
---
## Patch 3/5: eal: fix memory leak in async IPC secondary path
### Warnings
**Inconsistent dummy cleanup between primary and secondary paths**
The secondary process path now frees the unused dummy on the success path, but the primary process path does not have equivalent logic. Both paths should be consistent. Check whether the primary path also leaks `dummy` when `nb_sent > 0` and `dummy_used == false`.
---
## Patch 4/5: eal: fix async IPC resource leaks on partial failure
### Errors
**Use-after-free on partial rollback failure path**
In the partial failure rollback loop starting at line 1274, the code frees `r->reply` and `r` itself, but does NOT free `r->request`. The comment says `/* r->request == copy, freed below after the loop */`, but if multiple requests were queued, each has its own `pending_req->request` pointer. Only the last one (or first one, depending on allocation order) will equal `copy`. The others point to memory that was never allocated in this function -- they are all pointing to the same `copy` which is freed once after the loop, but each `pending_req` was allocated separately and may have had its own `request` field set.
Wait -- re-reading `mp_request_async()`: `pending_req->request = req;` where `req` is the `copy` parameter passed from the caller. So all `pending_req` in this batch DO share the same `copy` pointer. The comment is correct. However, the code should NULL out `r->request` after noting it, to make it clear it's not being freed in the loop:
```c
/* r->request == copy, will be freed below */
r->request = NULL;
free(r);
```
Actually, on closer inspection, this is not a bug. All `pending_req` in the batch point to the same `copy`, and `copy` is freed once at the end. The loop correctly frees `r->reply` (which is unique per request) and `r` itself, but leaves `r->request` alone since it's shared. The comment is accurate.
**However**, there IS a problem: the rollback only happens when `ret != 0 && reply->nb_sent > 0`. But `mp_request_async()` can fail (return -1) without setting `ret` in the caller's scope, or the caller's loop can have `ret = -1` without incrementing `nb_sent`. If `mp_request_async()` fails on the first peer, `reply->nb_sent` is still 0, but `pending_req` was added to the queue. The rollback condition `reply->nb_sent > 0` will be false, so those requests are not removed from the queue, but `copy` and `param` are freed at the end, causing a use-after-free when the alarm fires.
The rollback should happen whenever `ret != 0` and at least one request was queued in the current batch, not just when `nb_sent > 0`. The condition should check whether any `pending_req` with this `param` exists in the queue.
**Simpler fix**: Always roll back all requests with matching `param` when `ret != 0`, regardless of `nb_sent`. The current code assumes `nb_sent > 0` means "some requests were queued", but `nb_sent` is incremented in `mp_request_async()` after `send_msg()` succeeds, not after queueing. If `send_msg()` fails, the request is still queued (line 953: `TAILQ_INSERT_TAIL`) but `nb_sent` is not incremented. So the rollback condition is wrong.
Change:
```c
if (ret != 0 && reply->nb_sent > 0) {
```
to:
```c
if (ret != 0) {
```
and the rollback will correctly remove all queued requests from the current batch.
**Alarm already set before allocation**
The alarm is set at line 917 before allocating `pending_req` and `reply_msg`. If allocation fails, the alarm will fire with a stale ID that might match a future request, or it will fire and find nothing (harmless per the comment). But the function returns -1 without canceling the alarm. This is intentional per the design (alarms are never canceled), but it means the ID space can be exhausted faster. Not a correctness bug, but worth noting.
Actually, re-reading: the ID is a simple incrementing counter. If allocation fails, the ID is "burned" but the alarm will fire and find no matching request. Per the patch 5 comment, this is intentional. Not a bug.
**Missing alarm cancellation on `send_msg()` failure**
When `send_msg()` fails (line 940), `goto fail` frees `pending_req` and `reply_msg` but does not cancel the alarm that was set at line 917. The alarm will fire with an ID that no longer has a matching request (because `pending_req` was never inserted into the queue). Per the patch 5 design, this is intentional -- the alarm becomes a harmless noop. Confirm this is the intended behavior. If so, not a bug, just documenting the design.
Per patch 5: "Remove callback-side alarm cancellation. It is safe to do so, because any callback triggered without a pending request becomes a noop." So this is intentional. Not a bug.
---
## Patch 5/5: eal: avoid deadlock in async IPC alarm callback
### Warnings
**Potential alarm accumulation**
The patch removes all `rte_eal_alarm_cancel()` calls, meaning every alarm set will eventually fire even if the request is canceled, completed early, or rolled back. Over time, this could accumulate stale alarms firing and doing ID lookups that always return NULL. Each stale alarm acquires `pending_requests.lock`, does a `find_async_request_by_id()` scan, finds nothing, and returns. This is overhead but not a correctness bug.
Consider whether a high rate of request failures or rollbacks could cause alarm callback storms. The alarm callback path is:
```c
pthread_mutex_lock(&pending_requests.lock);
req = find_async_request_by_id(id);
if (req != NULL)
req = async_reply_handle_thread_unsafe(req);
pthread_mutex_unlock(&pending_requests.lock);
```
So each stale alarm holds the lock for the duration of the ID scan. If there are thousands of pending requests and hundreds of stale alarms firing concurrently, lock contention could be significant. Not a bug, but a potential performance issue under pathological conditions.
**ID overflow**
`next_request_id` is `unsigned long` which is 32 bits on some platforms and 64 bits on others. On 32-bit platforms, it can wrap after 4 billion requests. If it wraps while old requests are still pending, ID collisions can occur, causing a stale alarm to match a live request and incorrectly cancel it. Use `uint64_t` for the ID and cast to/from `uintptr_t` for the alarm arg.
---
## Summary
### Patch 1: Clean
### Patch 2: 1 Error (missing alarm cancellation on error path)
### Patch 3: 1 Warning (inconsistent dummy cleanup)
### Patch 4: 1 Error (rollback condition wrong, misses requests queued with nb_sent==0)
### Patch 5: 2 Warnings (alarm accumulation, ID overflow on 32-bit)
### Overall
The series correctly identifies and fixes several real bugs (log message, missing callback, memory leak), but introduces a subtle use-after-free risk in patch 4 due to an incorrect rollback condition, and patch 2 has an error path that does not clean up the alarm. Patch 5's design decision to never cancel alarms is reasonable for avoiding deadlock but has performance implications that should be documented or mitigated (ID overflow, stale alarm accumulation).
More information about the test-report
mailing list