|WARNING| [v6] net/af_xdp: fix shared UMEM refcount corruption

dpdklab at iol.unh.edu dpdklab at iol.unh.edu
Thu Aug 20 21:42:49 CEST 2026


Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/168151

_AI Code Review_

=== Patch Review: series.patch (via Claude) ===
Review date: 2026-08-20

# DPDK Patch Review

## Summary

This patch fixes critical resource management bugs in the AF_XDP shared UMEM implementation. The fixes address use-after-free, resource leaks, and race conditions. I have identified several issues that need to be addressed.

---

## Errors

### 1. Use-after-free on fq_bufs array after `rte_pktmbuf_free_bulk()`

**Location:** `xsk_configure()` cleanup path

**Issue:**
The `fq_bufs` array is a local stack variable that goes out of scope when the function returns. After calling `rte_pktmbuf_free_bulk(fq_bufs, reserve_size)`, the mbufs are freed, but the array itself is just stack memory and doesn't need explicit cleanup. However, the real issue is that `free_fq_bufs` can be set to `true`, then the first `reserve_fill_queue()` call can fail before clearing it, and we jump to `out_umem` where we free the mbufs--but those mbufs may have already been consumed by `reserve_fill_queue()` on a partial success before it returned an error.

**Current code:**
```c
free_fq_bufs = true;
#endif

/* reserve fill queue of queues not (yet) sharing UMEM */
if (reserve_before) {
	ret = reserve_fill_queue(rxq->umem, reserve_size, fq_bufs, &rxq->fq);
	free_fq_bufs = false;
	if (ret) {
		AF_XDP_LOG_LINE(ERR, "Failed to reserve fill queue.");
		goto out_umem;
	}
```

**Problem:**
If `reserve_fill_queue()` partially consumes some mbufs from `fq_bufs` before failing (e.g., it enqueues some but not all), then `free_fq_bufs` is still `true` and we attempt to free mbufs that were already handed to the fill queue. This is a double-free.

**Fix:**
Set `free_fq_bufs = false` **before** calling `reserve_fill_queue()`, not after on success. Or verify that `reserve_fill_queue()` is truly atomic (all-or-nothing) with respect to the `fq_bufs` array.

---

### 2. Missing error check after `rte_pktmbuf_alloc_bulk()`

**Location:** `xsk_configure()`, line ~1722

**Issue:**
```c
#ifndef ETH_AF_XDP_SHARED_UMEM
ret = rte_pktmbuf_alloc_bulk(rxq->umem->mb_pool, fq_bufs, reserve_size);
if (ret) {
	AF_XDP_LOG_LINE(DEBUG, "Failed to get enough buffers for fq.");
	goto out_umem;
}
free_fq_bufs = true;
#endif
```

The code checks `ret` and jumps to `out_umem` on failure, which is correct. However, the `free_fq_bufs` flag is set to `true` only on the success path (after the `if (ret)` check). This is actually correct--no error here on closer inspection.

**Correction:** No issue here; the `free_fq_bufs = true` is in the success path. Disregard this item.

---

### 3. Race condition on `umem->refcnt` read-check-increment sequence

**Location:** `xdp_umem_configure()`, lines ~1195-1217

**Issue:**
```c
if (umem != NULL) {
	/* Reject sharing once the UMEM is at capacity. */
	if (rte_atomic_load_explicit(&umem->refcnt,
			rte_memory_order_acquire) >= umem->max_xsks) {
		// ... log and return NULL
	}

	AF_XDP_LOG_LINE(INFO, "%s,qid%i sharing UMEM", ...);
	rte_atomic_fetch_add_explicit(&umem->refcnt, 1, rte_memory_order_release);
}
```

Between the `rte_atomic_load_explicit()` check and the `rte_atomic_fetch_add_explicit()` increment, another thread could increment the refcount, allowing the count to exceed `max_xsks`. This is a classic TOCTOU (time-of-check-time-of-use) race.

**Fix:**
Use `rte_atomic_compare_exchange` in a loop, or use `rte_atomic_fetch_add` and check the **old value** returned by the fetch_add to see if it was already at capacity:

```c
uint8_t old_refcnt = rte_atomic_fetch_add_explicit(&umem->refcnt, 1,
	rte_memory_order_release);
if (old_refcnt >= umem->max_xsks) {
	/* We incremented past the limit; undo and fail. */
	rte_atomic_fetch_sub_explicit(&umem->refcnt, 1,
		rte_memory_order_release);
	// ... log and return NULL
}
```

This ensures atomicity: if multiple threads race to increment, only one will observe `old_refcnt < max_xsks` and succeed; the others will see `old_refcnt >= max_xsks` after their increment and will roll back.

---

### 4. Unsigned overflow on refcount decrement check

**Location:** `eth_dev_close()` and `xsk_configure()` cleanup, lines ~1070, ~1856

**Issue:**
```c
if (rte_atomic_fetch_sub_explicit(&rxq->umem->refcnt, 1,
		rte_memory_order_acq_rel) - 1 == 0)
	xdp_umem_destroy(rxq->umem);
```

`rte_atomic_fetch_sub_explicit()` returns the **old value** before the subtraction. Subtracting 1 from that gives the new value. If the refcount is `uint8_t` and somehow underflows (should not happen if the code is correct, but defensive coding), `old_value - 1` wraps to 255 instead of going negative, and `== 0` is false, so we don't destroy. This is actually safe against underflow.

However, a clearer pattern is `old_value == 1` (meaning after the decrement it is zero):

```c
if (rte_atomic_fetch_sub_explicit(&rxq->umem->refcnt, 1,
		rte_memory_order_acq_rel) == 1)
	xdp_umem_destroy(rxq->umem);
```

This avoids the extra subtraction and makes it clearer that we are checking "was this the last reference?"

**Fix suggestion (clarity, not correctness):** Use `== 1` instead of `- 1 == 0`.

---

### 5. NULL dereference if `umem` is still NULL in cleanup path

**Location:** `xsk_configure()` cleanup, line ~1855

**Issue:**
```c
out_umem:
	/* Free fq_bufs that were allocated but never handed to the fill queue. */
	if (free_fq_bufs)
		rte_pktmbuf_free_bulk(fq_bufs, reserve_size);
	if (rte_atomic_fetch_sub_explicit(&rxq->umem->refcnt, 1,
			rte_memory_order_acq_rel) - 1 == 0)
		xdp_umem_destroy(rxq->umem);
```

If `xdp_umem_configure()` returns `NULL` (early in the function, e.g., `get_shared_umem()` fails or allocation fails before the refcount increment), then `rxq->umem` is `NULL` when we jump to `out_umem`. Dereferencing `rxq->umem->refcnt` is a NULL pointer dereference.

**Current mitigation:**
The early return on line ~1710 sets `rxq->umem = NULL` and returns immediately, so we never reach `out_umem` in that case. However, if `xdp_umem_configure()` fails internally (e.g., `xsk_umem__create()` fails after allocating the `umem` struct but before incrementing `refcnt`), the flow is unclear.

**Fix:**
Add a NULL check before dereferencing `rxq->umem` in the cleanup path:

```c
out_umem:
	if (free_fq_bufs)
		rte_pktmbuf_free_bulk(fq_bufs, reserve_size);
	if (rxq->umem != NULL &&
	    rte_atomic_fetch_sub_explicit(&rxq->umem->refcnt, 1,
			rte_memory_order_acq_rel) == 1)
		xdp_umem_destroy(rxq->umem);
	rxq->umem = NULL;
	txq->umem = NULL;
```

Or ensure `xdp_umem_configure()` always increments the refcount before returning a non-NULL `umem`, so the decrement is always safe. Trace the code to confirm.

---

## Warnings

### 6. Inconsistent memory ordering on shared refcount increment

**Location:** `xdp_umem_configure()`, line ~1217

**Issue:**
The patch uses `rte_memory_order_release` on the shared refcount increment:

```c
rte_atomic_fetch_add_explicit(&umem->refcnt, 1, rte_memory_order_release);
```

**Rationale from patch:** "release on the shared increment."

**Concern:**
Release ordering on an increment does NOT synchronize with the acquire load in the capacity check a few lines earlier (they are in the same thread, so no inter-thread ordering is established). The release ordering on the increment ensures that writes **before** the increment are visible to the thread that does the final acquire-release decrement. However, if the UMEM is already initialized and refcnt > 0, there's no data publication happening here--just a reference count bump.

**Correct ordering for a simple refcount increment (no associated data):** `rte_memory_order_relaxed`.

**Correct ordering for a refcount increment that publishes data:** `rte_memory_order_release` (but only if there's data to publish).

**Recommended fix:**
Unless the increment is publishing newly written UMEM state visible to other threads, use `rte_memory_order_relaxed`:

```c
rte_atomic_fetch_add_explicit(&umem->refcnt, 1, rte_memory_order_relaxed);
```

The final decrement already uses `acq_rel` to synchronize the destruction.

---

### 7. Missing bounds check on `reserve_size` variable

**Location:** `xsk_configure()`, line ~1707

**Issue:**
```c
int reserve_size = ETH_AF_XDP_DFLT_NUM_DESCS;
struct rte_mbuf *fq_bufs[reserve_size];
```

`reserve_size` is initialized to a constant (`ETH_AF_XDP_DFLT_NUM_DESCS`), so this is a variable-length array (VLA) on the stack. VLAs are acceptable in DPDK but can cause stack overflow if the constant is very large.

**Recommendation:**
Verify that `ETH_AF_XDP_DFLT_NUM_DESCS` is a reasonable size (e.g., 2048 or 4096). If it's 4096 and `sizeof(struct rte_mbuf *)` is 8, the array is 32 KB, which is large for the stack.

If this is a concern, consider using heap allocation or a fixed-size array with a compile-time assertion that the constant fits.

**Fix suggestion:**
Add a compile-time check or a comment justifying the stack usage:

```c
RTE_BUILD_BUG_ON(ETH_AF_XDP_DFLT_NUM_DESCS > 2048);
```

or allocate on heap if the size is too large.

---

### 8. Potential mempool exhaustion not logged clearly

**Location:** `xdp_umem_configure()`, line ~1199

**Issue:**
When the mempool is too small (max_xsks == 0), the log says:

```c
AF_XDP_LOG_LINE(ERR,
	"%s,qid%i: mempool %s has %u mbufs, "
	"need at least %u to share UMEM",
	internals->if_name, rxq->xsk_queue_idx,
	umem->mb_pool->name,
	umem->mb_pool->populated_size,
	ETH_AF_XDP_NUM_BUFFERS);
```

This is logged only when `max_xsks == 0` (mempool too small to share even one socket). If the mempool is large enough for one socket but not two, and the user tries to add a second queue, the `else` branch logs:

```c
AF_XDP_LOG_LINE(ERR,
	"%s,qid%i: UMEM %s already at max %u sockets",
	...);
```

This is clear enough. However, it might help to also log the mempool size in the "already at max" case, for debugging.

**Suggestion:**
Consider adding the mempool's populated size to the "already at max" log as well.

---

### 9. Documentation wording could be clearer

**Location:** `doc/guides/nics/af_xdp.rst`, line ~102

**Current:**
```rst
The shared mempool must be large enough for every AF_XDP socket sharing the UMEM.
Each socket needs 4096 mbufs for its fill queue, so ``N`` sockets need at least ``4096 * N`` mbufs.
Rx queue setup fails if the mempool is too small to add another socket to the UMEM.
```

**Suggestion:**
Clarify "4096 mbufs" as `ETH_AF_XDP_NUM_BUFFERS` in the code, so if the constant changes, the doc stays accurate. Also, one sentence per line (RST style):

```rst
The shared mempool must be large enough for every AF_XDP socket sharing the UMEM.
Each socket needs 4096 mbufs for its fill queue
(the value of ``ETH_AF_XDP_NUM_BUFFERS`` in the driver), so ``N`` sockets need at least ``4096 * N`` mbufs.
Rx queue setup fails with ``-ENOMEM`` if the mempool is too small to add another socket to the UMEM.
```

---

## Info

### 10. Consider using `RTE_MIN()` inline instead of ternary

**Location:** `xdp_umem_configure()`, line ~1263

**Current:**
```c
umem->max_xsks = RTE_MIN(mb_pool->populated_size / ETH_AF_XDP_NUM_BUFFERS,
			(uint32_t)UINT8_MAX);
```

This is already using `RTE_MIN()`, which is correct. No issue here.

---

### 11. Verbose logging on every shared queue setup

**Location:** `xdp_umem_configure()`, line ~1215

**Current:**
```c
AF_XDP_LOG_LINE(INFO, "%s,qid%i sharing UMEM", ...);
```

This logs at INFO level every time a queue shares the UMEM. If many queues are set up, this could be verbose. Consider DEBUG level or rate-limiting.

**Suggestion:** Change to DEBUG level unless it's important for users to see.

---

## Summary of Required Fixes

**Must fix (Errors):**

1. **TOCTOU race on refcount increment** (Error #3): Use `fetch_add` and check the old value atomically.
2. **NULL dereference in cleanup path** (Error #5): Add NULL check before `rxq->umem->refcnt`.
3. **Double-free risk if `reserve_fill_queue()` partially consumes `fq_bufs`** (Error #1): Verify `reserve_fill_queue()` atomicity or set `free_fq_bufs = false` before calling it.

**Should fix (Warnings):**

4. **Memory ordering on refcount increment** (Warning #6): Use `relaxed` unless publishing data.
5. **Refcount comparison clarity** (Error #4, reclassified as clarity): Use `== 1` instead of `- 1 == 


More information about the test-report mailing list