|WARNING| [RFC, 3/3] app/test-mempool-perf: add testing in pipeline model
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Thu Aug 20 16:50:04 CEST 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/168123
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-08-20
# DPDK Patch Review
## Patch 1/3: app/test-mempool-perf: skeleton of new test app
### Errors
**Use-after-free potential in interactive mode**
File: `app/test-mempool-perf/main.c`
In `run_interactive_mode()`, if `fgets()` returns `NULL` (EOF or error) after a successful mempool type entry, the function returns `-1` immediately without allowing the caller (`main()`) to proceed to cleanup. However, since this happens before any resources are allocated, it's not a leak--but the pattern invites future mistakes if resource allocation is moved earlier.
Current code flow is safe, but be aware: early returns without cleanup are error-prone if allocation moves earlier in the function.
---
**Missing error check on `rte_strscpy()` return value**
File: `app/test-mempool-perf/main.c`, line ~158
```c
rte_strscpy(cfg.mempool_type, buf, sizeof(cfg.mempool_type));
```
`rte_strscpy()` returns the number of bytes copied, or a negative error code if truncation occurred. The return value is not checked. If the user enters a mempool type name longer than `RTE_MEMPOOL_NAMESIZE - 1`, truncation is silent and the resulting name may not match any registered mempool, causing the subsequent `is_valid_mempool_type()` check to fail. This produces a confusing error message ("Unknown mempool type") when the real issue is truncation.
**Fix:** Check the return value and report truncation explicitly:
```c
if (rte_strscpy(cfg.mempool_type, buf, sizeof(cfg.mempool_type)) < 0) {
printf(" Mempool type name too long (max %zu chars)\n",
sizeof(cfg.mempool_type) - 1);
continue;
}
```
---
**Unchecked `rte_strscpy()` in `parse_args()`**
File: `app/test-mempool-perf/main.c`, lines ~278-280
```c
if (mempool_type_arg != NULL)
rte_strscpy(cfg.mempool_type, mempool_type_arg,
sizeof(cfg.mempool_type));
```
Same issue as above: truncation is not detected. If a user supplies a very long `--mempool-type` argument, the truncated name will fail the `is_valid_mempool_type()` check later, producing a misleading error.
**Fix:**
```c
if (mempool_type_arg != NULL) {
if (rte_strscpy(cfg.mempool_type, mempool_type_arg,
sizeof(cfg.mempool_type)) < 0) {
fprintf(stderr, "Error: mempool type name exceeds %zu characters\n",
sizeof(cfg.mempool_type) - 1);
return -1;
}
}
```
---
### Warnings
None.
---
## Patch 2/3: app/test-mempool-perf: add perf test logic
### Errors
**Resource leak on error path in `worker_main()`**
File: `app/test-mempool-perf/main.c`, lines ~300-314
If any of `hold_objs`, `delta`, or `tmp` allocation fails, the function frees only the successfully allocated pointers and returns. However, if `hold_objs` succeeds but `delta` fails, then `tmp` is never allocated and the `free(tmp)` on line 313 is safe (free(NULL) is a no-op). The same applies to the other orderings.
**Actually correct.** The code is safe: `malloc()` failure leaves the pointer uninitialized (it retains its declaration value, which is the result of the previous malloc or NULL), and `free(NULL)` is well-defined. Disregard this item.
---
**Use of `volatile` for shared variable access**
File: `app/test-mempool-perf/main.c`, line ~286
```c
static void
access_object(void *obj)
{
volatile uint64_t *p = (volatile uint64_t *)obj;
uint64_t acc = 0;
uint32_t i;
for (i = 0; i < ELEM_SIZE / sizeof(uint64_t); i++)
acc += p[i];
for (i = 0; i < RTE_CACHE_LINE_SIZE / sizeof(uint64_t); i++)
p[i] = acc;
}
```
The `volatile` qualifier on `p` prevents the compiler from optimizing away the reads and writes, which is the intent here (to touch memory and simulate workload access). However, this is a **misuse of `volatile` in the AGENTS.md sense** only if the data were shared between threads for synchronization purposes. In this context, `volatile` is being used to force memory access (to measure memory bandwidth pressure), not for atomicity or ordering between threads. Each worker thread has exclusive ownership of the objects it allocated.
**Conclusion:** This is acceptable use of `volatile`--it's forcing memory access within a single thread's context, not attempting inter-thread synchronization. Do not flag this.
---
**Infinite loop if pool exhausted**
File: `app/test-mempool-perf/main.c`, lines ~320-348
```c
while (rte_atomic_load_explicit(&test_running, rte_memory_order_relaxed)) {
if (run >= RESHUFFLE_INTERVAL) {
shuffle_and_validate(delta, tmp, rf);
run = 0;
}
for (i = 0; i < rf; i++) {
if (delta[i] > 0) {
if (rte_mempool_get_bulk(mp, hold_objs + hold_count, bs) != 0) {
stats->get_fail++;
continue; /* <-- skips the +bs to hold_count */
}
/* ... access objects ... */
hold_count += bs;
stats->get_success += bs;
} else {
hold_count -= bs; /* <-- assumes we have bs objects to free */
rte_mempool_put_bulk(mp, hold_objs + hold_count, bs);
stats->put_count += bs;
}
}
run++;
}
```
If an allocation fails (`get_bulk` returns != 0), the code increments `get_fail` and `continue`s, skipping the `hold_count += bs`. On the next iteration, if `delta[i]` is `-1` (free), the code executes `hold_count -= bs` and tries to free `bs` objects--but we never added those objects because the allocation failed. This underflows `hold_count` (wraps to a huge value) and causes `rte_mempool_put_bulk()` to be called with a wildly out-of-bounds pointer (`hold_objs + hold_count`), which is **undefined behavior** and will likely crash.
**Fix:** Track allocation success explicitly or check that `hold_count >= bs` before attempting to free:
```c
if (delta[i] > 0) {
if (rte_mempool_get_bulk(mp, hold_objs + hold_count, bs) == 0) {
if (cfg.access_on_alloc) {
for (k = 0; k < bs; k++)
access_object(hold_objs[hold_count + k]);
}
hold_count += bs;
stats->get_success += bs;
} else {
stats->get_fail++;
}
} else {
/* Only free if we actually have objects */
if (hold_count >= bs) {
hold_count -= bs;
rte_mempool_put_bulk(mp, hold_objs + hold_count, bs);
stats->put_count += bs;
}
/* If hold_count < bs, we can't free--skip this iteration */
}
```
Alternatively, rebalance the delta array to skip frees that can't be satisfied, but the simpler fix is the check above.
---
**Missing return value check on `rte_mempool_populate_default()`**
File: `app/test-mempool-perf/main.c`, lines ~377-383
```c
ret = rte_mempool_populate_default(mp);
if (ret < 0) {
fprintf(stderr, "Failed to populate mempool: %s\n",
rte_strerror(-ret));
rte_mempool_free(mp);
return NULL;
}
```
This check is present and correct. No issue.
---
### Warnings
**`hold_objs` allocation should use `rte_zmalloc_socket()`**
File: `app/test-mempool-perf/main.c`, line ~300
```c
hold_objs = malloc(bs * (rf / 2) * sizeof(*hold_objs));
```
This is an array of pointers to mempool objects. The pointers themselves are process-private and do not need to be in hugepage memory. Standard `malloc()` is acceptable here per the guidelines ("Use `rte_malloc()` only when required").
**Actually acceptable.** Do not flag.
---
**`delta` and `tmp` arrays are control structures, not mempool elements**
Same reasoning as above: these are process-private arrays, not DMA buffers or shared memory. `malloc()` is appropriate.
---
## Patch 3/3: app/test-mempool-perf: add testing in pipeline model
### Errors
**Resource leak on error path in `run_pipeline_test()`**
File: `app/test-mempool-perf/main.c`, lines ~620-641
```c
pargs = malloc(nb_pairs * sizeof(*pargs));
rings = malloc(nb_pairs * sizeof(*rings));
if (pargs == NULL || rings == NULL)
rte_exit(EXIT_FAILURE, "Failed to allocate pipeline resources\n");
for (i = 0; i < nb_pairs; i++) {
snprintf(ring_name, sizeof(ring_name), "pipe_ring_%u", i);
rings[i] = rte_ring_create(ring_name, PIPELINE_RING_SIZE,
rte_socket_id(),
RING_F_SP_ENQ | RING_F_SC_DEQ);
if (rings[i] == NULL)
rte_exit(EXIT_FAILURE,
"Failed to create pipeline ring %u: %s\n",
i, rte_strerror(rte_errno));
pargs[i].mp = mp;
pargs[i].ring = rings[i];
}
```
If `rte_ring_create()` fails on iteration `i > 0`, the function calls `rte_exit()` immediately. This abandons the rings created in iterations `0..i-1` without calling `rte_ring_free()` on them, leaking those ring resources.
**Fix:** Before calling `rte_exit()`, iterate over `0..i-1` and free the successfully created rings:
```c
if (rings[i] == NULL) {
for (uint32_t j = 0; j < i; j++)
rte_ring_free(rings[j]);
free(pargs);
free(rings);
rte_exit(EXIT_FAILURE,
"Failed to create pipeline ring %u: %s\n",
i, rte_strerror(rte_errno));
}
```
---
**Resource leak on `malloc()` failure in `producer_main()`**
File: `app/test-mempool-perf/main.c`, lines ~399-407
```c
static int
producer_main(void *arg)
{
/* ... */
void **objs;
objs = malloc(bs * sizeof(*objs));
if (objs == NULL)
return -ENOMEM;
```
If `malloc()` fails, the function returns `-ENOMEM` to the remote launch caller. The remote launch wrapper does not free `pargs` or `rings` in the main thread. However, `pargs` and `rings` are allocated in `run_pipeline_test()`, which waits for all workers with `rte_eal_mp_wait_lcore()` and then frees them after the drain loop. So this is **not a leak**--the main thread always cleans up its own allocations.
**Actually correct.** Do not flag.
---
**Same reasoning applies to `consumer_main()`**
The `malloc()` failure in `consumer_main()` also returns to the caller, and the main thread cleans up `pargs` and `rings` after the test completes. No leak.
---
**Busy-wait in `producer_main()` may starve other threads**
File: `app/test-mempool-perf/main.c`, lines ~417-424
```c
while (rte_ring_enqueue_bulk(ring, objs, bs, NULL) == 0) {
rte_pause();
if (!rte_atomic_load_explicit(&test_running, rte_memory_order_relaxed)) {
rte_mempool_put_bulk(mp, objs, bs);
goto done;
}
}
```
This spin-waits until the ring has space. If the consumer is slower than the producer, the producer will spin here. This is intentional--it's a performance test measuring steady-state throughput under backpressure. The `rte_pause()` reduces CPU contention. This is **not a bug**.
---
### Warnings
**Array iteration after `rte_exit()` is unreachable**
File: `app/test-mempool-perf/main.c`, lines ~655-658
The cleanup code after `rte_exit()` in the ring creation loop is unreachable because `rte_exit()` terminates the process. The fix suggested above (freeing rings before `rte_exit()`) is the correct approach.
---
## Summary of Findings by Patch
### Patch 1/3
**Errors:**
1. Missing `rte_strscpy()` return check in `run_interactive_mode()` (line ~158)
2. Missing `rte_strscpy()` return check in `parse_args()` (lines ~278-280)
**Warnings:** None
---
### Patch 2/3
**Errors:**
1. Hold count underflow in `worker_main()` when allocation fails (lines ~320-348)
**Warnings:** None
---
### Patch 3/3
**Errors:**
1. Resource leak on ring creation failure in `run_pipeline_test()` (lines ~620-641)
**Warnings:** None
---
## General Observations
- The patches introduce a new application, not a library, so release notes are not required.
- No ABI changes.
- No usage of deprecated API detected.
- Commit message formatting and SPDX/copyright are not reviewed per instructions.
- The use of `volatile` in `access_object()` is intentional (forcing memory access) and acceptable--it's not inter-thread synchronization.
- The use of `malloc()` for process-private control structures is correct per guidelines.
- The pipeline mode test correctly models producer/consumer patterns with rings.
More information about the test-report
mailing list