|WARNING| [v3] examples: add Wycheproof validation app
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Thu Sep 17 18:56:57 CEST 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/169752
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-09-17
# DPDK Wycheproof Validation Example - Code Review
## Summary
This patch adds a new example application for validating cryptodev PMDs against Google Wycheproof test vectors. The code quality is generally good, with proper resource management and clear structure. However, there are **several critical correctness bugs** related to resource leak patterns, error handling, and inappropriate API usage that must be fixed.
---
## ERRORS (Must Fix)
### Correctness Bugs
1. **Error path resource leak - session allocation failure**
`run_aead()` lines 611-613:
```c
session = rte_cryptodev_sym_session_create(env.dev_id, &xform, env.session_pool);
if (session == NULL)
goto out;
```
If session creation fails after `aad` is allocated (line 605-610 for CCM), the `goto out` leaks `aad` because the free path (line 654) is unreachable when `session == NULL` prevents the enqueue. The cleanup at `out:` (line 654) frees `aad`, but if we jump to `out` before allocating `mbuf` or `*digest`, and then fail before the enqueue check, the `aad` allocation may leak in error paths that short-circuit.
**Actually, reviewing more carefully:** The `aad` allocation happens at lines 605-610, which is after the session check. So if session creation fails, we haven't allocated `aad` yet. This is actually correct as written. **Withdraw this item.**
2. **Use-after-free in asym session creation failure path**
`run_ecdh_ecpoint()` lines 1512-1517:
```c
if (rte_cryptodev_asym_session_create(env.dev_id, &xform, env.asym_session_pool,
&session) < 0 || session == NULL) {
ret = -EIO;
goto out;
}
```
Comment at line 1513 says "Session-create failure after the capability check is a PMD failure", but then at cleanup (line 1556-1557):
```c
if (session != NULL)
rte_cryptodev_asym_session_free(env.dev_id, session);
```
If `rte_cryptodev_asym_session_create()` returns success (>= 0) but sets `session == NULL`, the condition `|| session == NULL` triggers `goto out`, bypassing the session attachment. Later, at cleanup, `session != NULL` is false, so we don't free. But the create call succeeded, meaning a session was allocated somewhere. This is a resource leak, not use-after-free. However, the real question is: **can `rte_cryptodev_asym_session_create()` return >= 0 but leave `session == NULL`?**
Checking the API: `rte_cryptodev_asym_session_create()` returns `int` (0 on success, negative on failure) and writes `session` via pointer. If it returns 0, `session` is guaranteed valid. So the `|| session == NULL` check is defensive against a buggy PMD. If a PMD violates the contract (returns 0 but `session` is NULL), the defensive check prevents a NULL dereference but doesn't free the phantom session. However, this is a PMD bug, not an application bug. The defensive check is reasonable. **No issue here; defensive programming is acceptable.**
3. **Error path resource leak - `hash_qp` setup allocates resources but not freed on error**
`app_init()` lines 255-270:
```c
for (d = 0; d < count; d++) {
struct rte_cryptodev_qp_conf hash_qp = { 128, NULL };
if (d == env.dev_id)
continue;
if (rte_cryptodev_sym_capability_get(d, &hash_idx) == NULL)
continue;
if (rte_cryptodev_configure(d, &config) < 0)
continue;
hash_qp.mp_session = env.session_pool;
if (rte_cryptodev_queue_pair_setup(d, 0, &hash_qp,
rte_socket_id()) < 0 ||
rte_cryptodev_start(d) < 0) {
rte_cryptodev_close(d); // <-- closes configured device
continue;
}
env.hash_dev_id = d;
env.hash_dev_own = true;
break;
}
```
If `rte_cryptodev_queue_pair_setup()` succeeds but `rte_cryptodev_start()` fails, the `||` short-circuits and we call `rte_cryptodev_close(d)`. However, `rte_cryptodev_close()` may not clean up the queue pair if the device was never started. The safe pattern is to call `rte_cryptodev_stop()` before `close()`, but you can only stop a started device. Here, if `start()` fails, the device is configured but not started, so `close()` should clean it up. Checking DPDK docs: `rte_cryptodev_close()` releases all resources; calling it on a configured-but-not-started device is valid. **This is actually correct.** Withdraw.
4. **Missing error check on `mempool_free()`**
`app_uninit()` lines 293-297:
```c
rte_mempool_free(env.asym_op_pool);
rte_mempool_free(env.asym_session_pool);
rte_mempool_free(env.op_pool);
rte_mempool_free(env.session_pool);
rte_mempool_free(env.mbuf_pool);
```
Wait, `rte_mempool_free()` returns `void`. There's no error to check. **Withdraw this item.**
5. **Resource leak on `readdir()` loop early exit**
`process_path()` lines 1907-1927:
```c
directory = opendir(path);
if (directory == NULL)
return -errno;
while ((entry = readdir(directory)) != NULL) {
// ... processing ...
ret = process_file(file_path, stats);
if (ret != 0)
break;
}
closedir(directory);
return ret;
```
If `process_file()` returns non-zero, we `break` and then call `closedir()`. The `closedir()` is outside the loop, so it IS called. **This is correct.** Withdraw.
6. **AAD allocation without checking vector length bounds**
`run_aead()` lines 605-610 (CCM case):
```c
if (algorithm == RTE_CRYPTO_AEAD_AES_CCM) {
aad = rte_zmalloc(NULL, RTE_ALIGN_CEIL(vector->aad_len + 18, 16), 0);
if (aad == NULL)
goto out;
if (vector->aad_len != 0)
memcpy(aad + 18, vector->aad, vector->aad_len);
```
If `vector->aad_len` is near `UINT32_MAX`, then `vector->aad_len + 18` overflows to a small value, causing `rte_zmalloc()` to allocate a tiny buffer. Then `memcpy(aad + 18, ...)` writes `vector->aad_len` bytes, overflowing the buffer.
However, looking at where `aad_len` comes from: `decode_hex()` at line 394 checks `hex_length / 2 > UINT32_MAX` and rejects. So `aad_len` is at most `UINT32_MAX`. Adding 18 overflows if `aad_len > UINT32_MAX - 18`. But `decode_hex()` already capped it at `UINT32_MAX / 2` (since `hex_length` must be even and `hex_length / 2` is stored). So `aad_len` is at most `(UINT32_MAX - 1) / 2`, well below the overflow threshold. **This is actually safe.** Withdraw.
7. **Unchecked return value from `snprintf()` allowing path truncation**
`process_path()` lines 1918-1923:
```c
if (snprintf(file_path, sizeof(file_path), "%s/%s", path, entry->d_name) >=
(int)sizeof(file_path)) {
ret = -ENAMETOOLONG;
break;
}
```
This correctly checks for truncation (`snprintf` returns the number of characters that would have been written, so `>= sizeof` means truncation occurred). **This is correct.** Withdraw.
8. **`rte_malloc()` used in control path instead of `malloc()`**
Throughout the code, `rte_malloc()` / `rte_zmalloc()` are used for temporary buffers (AAD, digests, operands). Per the guidelines, `rte_malloc()` should be reserved for DMA-accessible memory or shared-process memory. Many of these buffers are passed to `rte_malloc_virt2iova()`, indicating they need physical addresses, so they MUST be from `rte_malloc()`. **This is correct usage.** Withdraw.
9. **`normalized_operand()` allows value_len == bytesize, but bytesize may be > output buffer**
Actually, looking at callers:
- `run_ecdh_ecpoint()` line 1504: `normalize_operand(priv, priv_len, priv_buf, bytesize)` where `priv_buf` is `uint8_t priv_buf[MAX_EC_BYTESIZE]` (line 1499). The check `if (bytesize > sizeof(priv_buf))` at line 1501 ensures `bytesize <= MAX_EC_BYTESIZE`. So the buffer is sized correctly.
- Similar for ECDSA. **This is correct.** Withdraw.
After thorough review, I found no actual correctness bugs. The code has good error path coverage and resource cleanup. Let me check for other classes of errors.
10. **Implicit comparison in boolean context**
Per AGENTS.md, comparisons must be explicit. Checking for `if (ptr)` that should be `if (ptr != NULL)`:
- Line 614: `if (session == NULL)` - explicit, good
- Line 617: `if (operation_op == NULL)` - explicit, good
- Line 620: `if (mbuf == NULL)` - explicit, good
- Many others are explicit.
But wait, checking actual boolean truthiness:
- Line 191: `if (env.mbuf_pool == NULL)` - explicit
- Line 629: `if (input_len != 0)` - explicit
- Line 632: `if (*digest == NULL && vector->tag_len != 0)` - explicit
All comparisons appear to be explicit. **No issues.**
11. **Process-shared mutex without `PTHREAD_PROCESS_SHARED`**
Searching for `pthread_mutex`, `pthread_cond`, `pthread_rwlock`: None found in this patch. **Not applicable.**
12. **Shared variable access without atomics**
The `env` global is written in `main()` during single-threaded initialization and read in worker paths. There are no worker threads; the code is single-threaded. `env` is not modified after initialization completes. **Not applicable.**
After exhaustive review, I found **one actual Error**:
### Process Compliance Error
13. **New example without release notes**
The patch adds a new example application but does not update the current release notes file. Per guidelines, new examples require a release notes entry.
---
## WARNINGS (Should Fix)
1. **Debug output uses `printf()` in example code**
`printf()` is forbidden in `lib/` and `drivers/`, but this is `examples/`, where it's acceptable for user-facing output. **Not an issue.**
2. **`strcmp(vector->result, "valid")` repeated pattern could use helper**
The pattern `strcmp(vector.result, "valid") == 0` appears dozens of times. A helper `is_result(vector, "valid")` would reduce repetition and potential typos. This is a maintainability suggestion, not an error.
3. **Long functions exceed readability threshold**
Functions like `process_ecdh_ecpoint()` (100+ lines), `process_ecdsa_p1363()`, and `process_dsa_p1363()` are very long with deep nesting. Consider extracting vector validation logic into separate functions.
4. **Magic numbers for sizes**
- Line 31: `#define IV_OFFSET (sizeof(struct rte_crypto_op) + sizeof(struct rte_crypto_sym_op))` - good, computed.
- Line 30: `#define MAX_EC_BYTESIZE 66` - magic number. Should be `(521 + 7) / 8` with a comment about secp521r1.
- Line 29: `#define MAX_IV_LEN 512` - magic, but acceptable for examples.
5. **Inconsistent error message capitalization**
- Line 184: `"Cannot initialize EAL\n"` - capitalized
- Line 1946: `"Cannot initialize cryptodev %u: %s\n"` - capitalized
- Line 1951: `"Vector processing failed: %s\n"` - capitalized
Consistent, actually. **Withdraw.**
6. **Function `compute_hash()` has unclear ownership of `op_digest`**
Lines 1191-1196: `op_digest` is allocated with `rte_malloc()`, used, then freed before return. Output is copied to caller's buffer. Clean pattern, no issue.
7. **Redundant NULL check before `rte_free()`**
Per guidelines, NULL checks before `rte_free()` are unnecessary (it's a no-op on NULL). However, coccinelle handles this mechanical cleanup; AI review should not flag it unless high confidence. Many instances: lines 319-324, etc. **Suppress per guidelines** (this is a mechanical pattern, not a logic error).
---
## INFO (Consider)
1. **Consider using `RTE_VERIFY()` for assertions**
Several capability checks could use `RTE_VERIFY()` to catch logic errors during development, e.g., ensuring `bytesize <= MAX_EC_BYTESIZE` before `normalize_operand()` calls.
2. **Dequeue timeout could be configurable**
`dequeue_one()` hardcodes a 1-second timeout (line 474). For slow PMDs or debug builds, this might be insufficient. Consider `--timeout` option.
3. **Consider Doxygen for exported structs**
`struct app_env`, `struct wycheproof_data`, etc. could have Doxygen comments for maintainability, though this is optional for examples.
4. **Memory pool sizing could be smarter**
Fixed pool sizes (64 mbufs, 128 queue depth) are reasonable defaults but may be insufficient for large test suites. Not critical for an example.
5. **SHA-2 and SHA-3 hash mapping could use a table**
`map_sha()` has a long if-else chain. A static lookup table would be more maintainable.
---
## FINAL REVIEW CHECKLIST
- [x] Resource leaks on error paths: None found after thorough review
- [x] Use-after-free: None found
- [x] Race conditions: Not applicable (single-threaded)
- [x] `volatile` for inter-thread sync: Not applicable
- [x] Explicit comparisons: All correct
- [x] Process-shared primitives: Not applicable
- [x] Unchecked error returns: Only benign cases (`mempool_free`, always-success functions)
- [x] Style compliance: Good overall
- [ ] Release notes: **Missing** (Error)
---
## RECOMMENDATION
The code is well-written with good resource management and error handling. The primary issue is:
### Must Fix:
More information about the test-report
mailing list