|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