|WARNING| [v3] examples/rpcapd: demo version of packet capture daemon
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Wed Sep 23 00:04:54 CEST 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/169946
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-09-22
# DPDK Patch Review: examples/rpcapd
## Errors
### 1. Resource leak on error path in `dpdk_init()`
**Location:** `examples/rpcapd/main.c:1333-1368`
The `dpdk_init()` function allocates strings in a loop:
```c
for (i = 0; i < RTE_DIM(args); i++) {
eal_argv[i] = strdup(args[i]);
if (eal_argv[i] == NULL)
return -1;
}
```
If any `strdup()` fails, the function returns without freeing previously allocated strings or the `eal_argv` array itself. Later allocations have the same problem.
**Fix:** Add cleanup code before each `return -1`:
```c
for (i = 0; i < RTE_DIM(args); i++) {
eal_argv[i] = strdup(args[i]);
if (eal_argv[i] == NULL) {
while (i > 0)
free(eal_argv[--i]);
free(eal_argv);
return -1;
}
}
```
### 2. Use-after-free in `handle_updatefilter()`
**Location:** `examples/rpcapd/main.c:867`
When `read_filter()` reports a malformed request:
```c
ret = read_filter(fd, plen, s);
if (ret != 0) {
/* Malformed request: keep running with the old filter. */
rte_free(s->prm);
s->prm = old;
return ret < 0 ? -1 : 0;
}
```
The new `s->prm` allocated by `read_filter()` is freed, then `old` is restored to `s->prm`. But `read_filter()` may fail *before* allocating a new filter, leaving `s->prm == NULL`. Freeing NULL is safe, but the problem is: if `read_filter()` succeeds in allocating a filter but then fails validation (`bpf_validate()` rejects it), the newly allocated `s->prm` is leaked because the code frees it thinking it's the new one, then restores `old`, but the new `s->prm` was never saved.
Actually, reviewing more carefully: `read_filter()` sets `s->prm` to the new filter on success, leaves it NULL if allocation fails, and frees the new filter internally if validation fails. The code at line 867 frees `s->prm` (which is either NULL or the successfully allocated but not-yet-validated filter), then restores `old`. This is correct if `read_filter()` always leaves `s->prm` in a consistent state.
On closer inspection, `read_filter()` at line 603 does:
```c
s->prm = rte_bpf_convert(&bf);
free(insns);
if (s->prm == NULL) {
RPCAPD_LOG(ERR, "rte_bpf_convert failed: %s",
rte_strerror(rte_errno));
return rpcap_send_error(fd, 0, "cannot convert filter") < 0 ? -1 : 1;
}
```
So if `rte_bpf_convert()` fails, `s->prm` is NULL and the function returns non-zero. Then in `handle_updatefilter()`, `s->prm` (NULL) is freed, and `old` is restored. This is correct.
However, there is a different problem: in `handle_updatefilter()`, the line:
```c
struct rte_bpf_prm *old = s->prm;
s->prm = NULL;
ret = read_filter(fd, plen, s);
```
This saves `old`, sets `s->prm` to NULL, then calls `read_filter()`. If `read_filter()` succeeds, `s->prm` is set to a new filter. If it fails after allocating, the new filter is already in `s->prm` but the error path at line 867 frees it and restores `old`. This is correct.
The actual bug is: if `read_filter()` allocates a new filter, stores it in `s->prm`, but then `rte_bpf_convert()` fails and returns non-zero, the error path does `rte_free(s->prm)` (freeing the newly allocated filter), then `s->prm = old`, which is correct. So this is not a use-after-free.
Re-reading the code, I don't see a use-after-free here. The logic appears correct. Withdrawing this item.
### 3. Missing error check on `rte_eth_stats_get()` in `handle_stats()`
**Location:** `examples/rpcapd/main.c:1052`
```c
if (s->capture_on)
rte_eth_stats_get(s->port, &es);
```
`rte_eth_stats_get()` can fail (returns negative on error), but the return value is not checked. If it fails, `es` is left zero-initialized (which was done at declaration), and zero stats are sent to the client, which is misleading.
**Fix:**
```c
if (s->capture_on && rte_eth_stats_get(s->port, &es) < 0) {
RPCAPD_LOG(WARNING, "failed to get stats for port %u", s->port);
/* es remains zero-initialized; proceed with zero stats */
}
```
---
## Warnings
### 1. `rte_malloc()` used for control-path structure
**Location:** `examples/rpcapd/main.c:435`
```c
nb = realloc(buf, buflen + entry);
```
Standard `realloc()` is used here, which is correct for control-path allocations. However, the comment in the guidelines suggests `rte_malloc()` is for DMA/hugepage-backed memory. This allocation is for building a response message, so standard `realloc()` is appropriate.
This is not an issue. Do not flag.
### 2. Queue descriptor ring allocated with `rte_ring_create()` instead of `rte_zmalloc_socket()`
**Location:** `examples/rpcapd/main.c:506-511`
```c
static struct rte_ring *
create_capture_ring(uint16_t port)
{
char name[RTE_RING_NAMESIZE];
snprintf(name, sizeof(name), "rpcapd_r_%u_%d", port, getpid());
return rte_ring_create(name, ring_size, rte_socket_id(), 0);
}
```
`rte_ring_create()` internally uses `rte_zmalloc()`, so the ring is zero-initialized and NUMA-local. The capture ring is accessed by the pdump callback (which runs in the primary process context) and by this secondary process, so it must be in shared memory. `rte_ring_create()` handles this correctly. No issue.
### 3. Missing release notes entry for internal/experimental status
**Location:** `doc/guides/rel_notes/release_26_11.rst:68-71`
The release notes entry says:
```rst
* **Added an example of tcpdump remote pcap daemon.**
Added an example that implements rpcap to allow live capture in tcpdump.
```
The documentation (`doc/guides/sample_app_ug/rpcapd.rst:46-47`) clearly states:
```rst
* ``dpdk-rpcapd`` is experimental and provided for demonstration purposes only.
It may change or be removed without notice...
```
However, the release notes do not mention the experimental status. For an example application (not a library or driver), experimental status is less critical, but given the security warnings and the "demonstration purposes only" language, it would be clearer to note this in the release notes.
**Suggestion:** Add a note to the release notes entry:
```rst
* **Added an example of tcpdump remote pcap daemon.**
Added an example that implements rpcap to allow live capture in tcpdump.
This is a demonstration application and is not intended for production use.
```
### 4. DPDK BPF filter cleanup on `handle_updatefilter()` error path
**Location:** `examples/rpcapd/main.c:867-871`
In `handle_updatefilter()`, when `read_filter()` returns an error:
```c
if (ret != 0) {
rte_free(s->prm);
s->prm = old;
return ret < 0 ? -1 : 0;
}
```
If the capture is running (`s->capture_on == true`), the filter replacement disables and re-enables pdump. If the re-enable fails, `stop_capture()` is called, which frees `s->prm`. But in the error path above (which happens before the capture is running), `s->prm` is freed and `old` is restored. However, if we're keeping the old filter, we should not free it.
Wait, re-reading: at line 856, `old = s->prm; s->prm = NULL;`. So `old` holds the old filter, and `s->prm` is cleared. `read_filter()` may or may not allocate a new filter into `s->prm`. If it returns error, the new `s->prm` (if allocated) is freed, and `old` is restored. If `read_filter()` allocated a new filter, that filter is freed. If it didn't, `s->prm` is NULL and `rte_free(NULL)` is a no-op. Then `old` is restored. This is correct.
No issue here.
---
## Informational
### 1. Hardcoded Ethernet overhead and DLT type
**Location:** `examples/rpcapd/main.c:451` and `rpcap-protocol.h:59`
The code reports `DLT_EN10MB` (Ethernet) as the only link type and uses a fixed `DEFAULT_SNAPLEN` of `RTE_MBUF_DEFAULT_DATAROOM`. DPDK ports may have different link types (e.g., Linux TAP devices, raw IP interfaces), and their MTU/overhead may differ. However, for a demonstration application focused on typical Ethernet use cases, this is acceptable. The documentation notes limitations, so this is informational only.
### 2. Documentation clarity on "sample application"
**Location:** `doc/guides/sample_app_ug/rpcapd.rst`
The documentation is thorough and includes prominent warnings about unauthenticated access and non-production use. The security warning is repeated multiple times, which is good. One minor improvement: the phrase "demonstration purposes only" could be strengthened by explicitly stating that the application lacks production-level error handling, logging, and security features expected in a real daemon.
**Suggestion (optional):** Add a sentence in the warning section:
```rst
This sample application omits features required for production use, including authentication, encryption, audit logging, and robust multi-client handling.
```
### 3. `volatile` not used for `quit_signal`
**Location:** `examples/rpcapd/main.c:161-165`
The `quit_signal` variable is declared as `RTE_ATOMIC(bool)` and accessed via `rte_atomic_load_explicit()` and `rte_atomic_store_explicit()` with appropriate memory ordering. This is correct and follows the guidelines. No `volatile` is used, which is correct for inter-thread synchronization.
---
## Summary
**Critical Issues (Errors):**
- Resource leak in `dpdk_init()` on allocation failure (1 instance)
- Missing error check on `rte_eth_stats_get()` in `handle_stats()` (1 instance)
**Non-Critical Issues (Warnings):**
- Release notes could mention experimental/demonstration status (1 instance)
**Observations (Info):**
- Code correctly uses atomics for `quit_signal` (no volatile misuse)
- Hardcoded Ethernet DLT is acceptable for a demo application
- Security warnings in documentation are prominent and appropriate
The patch is well-structured and the code quality is good for a demonstration application. The critical issues are localized and straightforward to fix. The application correctly uses DPDK secondary process mechanisms, pdump, and the rpcap protocol for its intended demonstration purpose.
More information about the test-report
mailing list