|WARNING| [v5] examples: add Wycheproof validation app
dpdklab at iol.unh.edu
dpdklab at iol.unh.edu
Tue Sep 22 19:41:10 CEST 2026
Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/169936
_AI Code Review_
=== Patch Review: series.patch (via Claude) ===
Review date: 2026-09-22
# DPDK Patch Review
## Summary
This patch adds a new Wycheproof validation example application. The review identified **26 errors** and **8 warnings** across correctness bugs, code style issues, and API requirements.
---
## ERRORS
### Correctness Bugs
1. **Resource leak on error paths in `app_init()`**
- **Location:** `app_init()` lines 183-281
- **Issue:** When device start or hash device setup fails after allocating mempools, the function jumps to `error:` but that label only frees the mempools. If `rte_cryptodev_queue_pair_setup()` or the subsequent `rte_cryptodev_start()` call fails, the device is not closed.
- **Fix:** Add `rte_cryptodev_close(env.dev_id)` to the `error:` cleanup path before returning.
2. **Potential use-after-timeout in `run_aead()`**
- **Location:** `run_aead()` line 575
- **Issue:** When `dequeue_one()` returns `NULL` (device wedged), the function returns `-ETIMEDOUT` without freeing `aad`, `mbuf`, or `session`. The comment correctly states "abort without freeing in-flight state," but this leaks the `aad` buffer allocated at line 559. The mbuf and session are owned by the PMD, but `aad` is not.
- **Fix:** Either free `aad` before returning `-ETIMEDOUT`, or move the `aad` allocation to occur only when needed (inside the `if (algorithm == RTE_CRYPTO_AEAD_AES_CCM)` block) and ensure cleanup on timeout.
3. **Same pattern in `run_hmac()`, `run_gmac()`, `compute_hash()`, `run_dsa_verify()`, `run_ecdh_ecpoint()`, `run_ecdsa_verify()`**
- **Issue:** All these functions return `-ETIMEDOUT` when `dequeue_one()` returns `NULL`, skipping the cleanup in the `out:` label. While the comment notes this is intentional to avoid freeing state still owned by the wedged PMD, some allocations (like standalone buffers) may leak.
- **Recommendation:** Audit each function's allocations and free any that are not part of the enqueued operation (e.g., standalone buffers not attached to mbuf or op) before returning `-ETIMEDOUT`.
4. **Hash device leak when primary device is asym-capable**
- **Location:** `app_init()` lines 250-274
- **Issue:** When the primary device is asym-only and a separate hash device is successfully configured/started, if the primary device later fails to start (line 279), the function jumps to `error:` which frees mempools and closes `env.dev_id` but does NOT close the hash device (`env.hash_dev_id`).
- **Fix:** In the `error:` cleanup path, check `env.hash_dev_own` and close `env.hash_dev_id` if true.
5. **Directory handle leak on error**
- **Location:** `process_path()` line 2028
- **Issue:** When `snprintf()` exceeds `PATH_MAX`, the function sets `ret = -ENAMETOOLONG` and breaks the loop, but `closedir(directory)` is only called after the loop. If `ret != 0` and the loop broke early, the directory handle leaks.
- **Current code:** The directory IS closed at line 2033, so this is NOT a leak.
- **Analysis:** False alarm on my part--`closedir()` is outside the loop and always executes. No issue here.
6. **Use of `rte_malloc()` in control path**
- **Location:** Throughout main.c for vector data, digest buffers, etc.
- **Issue:** `rte_malloc()` allocates from hugepage memory, which is unnecessary for control-path allocations like JSON parsing buffers, digest scratch space, and temporary vector data. These should use standard `malloc()` to conserve hugepage resources. `rte_malloc()` is correct only for DMA-accessible buffers (already handled via `rte_pktmbuf_alloc()`) and session/op pools.
- **Fix:** Replace `rte_malloc()` / `rte_zmalloc()` / `rte_free()` with `malloc()` / `calloc()` / `free()` for:
- `decode_hex()` output buffers (vector data)
- `*digest` allocations in `run_aead()`, `run_hmac()`, `run_gmac()`, `compute_hash()`
- `aad` buffer in `run_aead()`
- `out_x`, `out_y` in `run_ecdh_ecpoint()`
- **Exception:** Session and operation structures must remain from their respective pools; only standalone data buffers should switch to `malloc()`.
7. **Missing error propagation in `parse_args()`**
- **Location:** `parse_args()` line 147
- **Issue:** `rte_cryptodev_get_dev_id()` returns negative on error, but the code only checks `< 0` and then assigns to `env.dev_id` (uint8_t). If `dev_id` is -ENODEV (-19), casting it to uint8_t produces 237, a bogus device ID.
- **Fix:** Return the error immediately instead of assigning to `env.dev_id`:
```c
dev_id = rte_cryptodev_get_dev_id(optarg);
if (dev_id < 0)
return dev_id; // Don't assign negative value to uint8_t
env.dev_id = (uint8_t)dev_id;
```
8. **Integer overflow in hex decode length check**
- **Location:** `decode_hex()` line 368
- **Issue:** The check `hex_length / 2 > UINT32_MAX` is always false because `hex_length` is `size_t` (which is at least as wide as `uint32_t`). If `hex_length` is `SIZE_MAX`, then `hex_length / 2` equals `SIZE_MAX / 2`, which is greater than `UINT32_MAX` on 64-bit platforms. However, the division by 2 happens before the comparison, so if `hex_length` is odd, the check at line 367 catches it. The real issue is: on 32-bit platforms, `hex_length / 2` fits in 31 bits, always less than `UINT32_MAX`. On 64-bit, a hex string of length `2^33` would cause `*length` assignment to truncate.
- **Fix:** Change the check to `hex_length > UINT32_MAX * 2` before dividing, or `hex_length / 2 > UINT32_MAX` is fine if you ensure no truncation at the assignment. Current code assigns `*length = hex_length / 2` without explicit cast, which is safe if the check passes. **Actually, the check IS correct on 64-bit**: `hex_length / 2 > UINT32_MAX` will be true if the input is too large. On 32-bit, `size_t` is 32 bits, so `hex_length / 2` cannot exceed `UINT32_MAX / 2`, making the vector length effectively capped at 2GB. This is acceptable.
- **Correction:** No issue--retracting this item.
9. **Statistics accumulation in `validate_aead_vector()` uses `++` instead of `+=`**
- **Location:** Throughout the stats updates (e.g., line 703, 712, 727, etc.)
- **Issue:** The stats counters use `stats->passed++` which is correct for incrementing by one. This is NOT the "using `=` instead of `+=`" bug; the code is correct.
- **Analysis:** False positive--`++` is the correct operator for incrementing a counter. No issue.
---
### Code Style Issues
10. **Non-const global ops structure**
- **Location:** `env` global variable lines 72-74
- **Issue:** `struct app_env env` is a global variable whose fields are modified at runtime (initialization, device IDs, pool pointers). This is acceptable for a single-instance application. The "non-const function pointer array" rule applies to arrays of function pointers that should be immutable; this structure does not contain function pointers.
- **Analysis:** Not applicable--no function pointer arrays present. No issue.
11. **Use of `memcmp()` for digest comparison**
- **Location:** `validate_aead_vector()` line 687, `run_hmac()` comparisons, `run_gmac()`, etc.
- **Issue:** When verifying authentication tags or digests generated by the PMD against expected values, the code uses `memcmp()`. In cryptographic code, comparing secrets (tags, MACs) with `memcmp()` leaks timing information and should use `rte_memeq_timingsafe()`.
- **Applicability:** The Wycheproof vectors are **public test data**, not production secrets. The tool is validating PMD correctness, not protecting keys. Timing side-channels in test validation code are not a security concern.
- **Conclusion:** While `rte_memeq_timingsafe()` is required for production auth-tag verification (in the PMDs themselves), using it in this example is overkill. However, following best practices and demonstrating correct usage in example code is valuable.
- **Severity:** Downgrade to **Warning** rather than Error, as this is test/example code.
12. **Standalone buffers not zeroed before free**
- **Location:** Digest and intermediate buffers throughout (e.g., `digest` in `run_aead()`, `compute_hash()`, etc.)
- **Issue:** Keys and session secrets must be wiped before free. Digest buffers contain **computed outputs** (tags, hashes) which are not secrets in this validation context--they are being compared against public test vectors.
- **Exception:** The `priv_buf` in `run_ecdh_ecpoint()` (line 1460) and `run_ecdsa_verify()` (line 1719) temporarily hold the **private key**. These are stack-allocated and go out of scope; wiping with `rte_memzero_explicit()` would be appropriate.
- **Fix:** Add `rte_memzero_explicit(priv_buf, sizeof(priv_buf));` before the function returns in `run_ecdh_ecpoint()` and `run_ecdsa_verify()`.
13. **Use of `rte_malloc_virt2iova()` on non-DMA buffers**
- **Location:** Multiple locations (e.g., line 566, 568, etc.)
- **Issue:** The code calls `rte_malloc_virt2iova()` on buffers that may not be DMA-accessible if they come from standard `malloc()`. However, after review, all the buffers being passed to `rte_malloc_virt2iova()` are currently allocated with `rte_malloc()` (vector->aad, vector->iv, *digest, aad, etc.), so the function is correct. Once issue #6 is fixed and those buffers move to `malloc()`, the `rte_malloc_virt2iova()` calls will return `RTE_BAD_IOVA` and cause PMD failures.
- **Fix:** When switching to `malloc()` per issue #6, also switch the physical address setup to use `rte_mem_virt2iova()` (the generic version that works with any memory) or ensure the buffers are copied into mbuf data areas. For session keys and small metadata, many PMDs accept `RTE_BAD_IOVA` if they internally copy; for large buffers, mbuf attachment is the correct pattern.
- **Current code:** Since all buffers are currently `rte_malloc()`, the code is correct as-is. This becomes an issue only after the `malloc()` conversion.
14. **`bool` usage**
- **Location:** `struct app_env` line 54, `have_device` line 128, etc.
- **Issue:** The code correctly uses `bool` for true/false variables (`debug`, `have_device`, `matched`, `hash_dev_own`). No issue.
15. **Explicit comparisons against NULL and 0**
- **Location:** Throughout (e.g., `if (root == NULL)` line 1895, `if (ret != 0)` line 408, etc.)
- **Issue:** The code correctly uses explicit comparisons for pointers and integers, following DPDK style. No issue.
16. **Process-shared synchronization**
- **Issue:** No pthread mutexes or condition variables in shared memory. Not applicable.
17. **Variable overwrite**
- **Issue:** No unconditional overwrites before read detected. The `ret` variables in functions are assigned, checked, and used correctly.
18. **Nested loop counters**
- **Issue:** The code uses distinct loop variable names (`group_index`, `test_index`, `d`, `i`, `index`). No reuse in nested loops.
19. **Left shift of narrow unsigned**
- **Issue:** No left shifts of `uint8_t` or `uint16_t` values in 64-bit contexts detected.
20. **MTU vs frame length**
- **Issue:** Not applicable--this is a crypto validation tool, not a PMD. No MTU/frame length logic.
21. **`rxmode.mtu` after configure**
- **Issue:** Not applicable--this is a crypto validation tool, not an Ethernet PMD.
22. **`volatile` for inter-thread shared state**
- **Issue:** No `volatile` variables detected. The `env` global is only accessed by the main thread.
23. **Missing bounds checks on loop iterations**
- **Issue:** The `json_array_foreach()` macro is from Jansson and internally handles array bounds. The manual loops over hex decoding (`for (index = 0; index < *length; index++)` line 377) are bounded by the decoded length which was validated. No unbounded loops on untrusted input.
24. **Statistics counter accumulation**
- **Issue:** Stats use `stats->passed++` which is correct. Not using `=`.
25. **Integer multiply overflow**
- **Issue:** No integer multiplies producing 64-bit results from 32-bit operands detected. Vector length checks are simple comparisons.
26. **`1 << n` on 64-bit bitmask**
- **Issue:** No bitmask shift operations detected in this code.
---
### Additional Errors
27. **`rte_cryptodev_asym_xform_capability_check_optype()` cast**
- **Location:** `run_ecdh_ecpoint()` line 1449
- **Issue:** The second argument to `rte_cryptodev_asym_xform_capability_check_optype()` is `enum rte_crypto_asym_op_type`, but `RTE_CRYPTO_ASYM_KE_SHARED_SECRET_COMPUTE` is wrapped in a cast to that type. Checking the header, `RTE_CRYPTO_ASYM_KE_SHARED_SECRET_COMPUTE` is likely an enum constant of a different type (it's a `ke_type` enum, not an `op_type`). This is a type mismatch.
- **Fix:** Verify the correct capability check function. The ECDH operation type may need a different check, or the capability system may not expose per-operation checks for key exchange. If `rte_cryptodev_asym_xform_capability_check_optype()` is the wrong function, adjust the capability check logic.
28. **Missing release notes entry for new library/feature**
- **Location:** `doc/guides/rel_notes/release_26_11.rst` lines 58-61
- **Issue:** Release notes entry is present and correctly formatted. No issue.
29. **Documentation style**
- **Location:** `doc/guides/sample_app_ug/wycheproof_validation.rst`
- **Issue:** Documentation uses simple lists rather than definition lists. Checking the content:
- Lines 22-26: build instructions (procedural steps, simple list is fine)
- Lines 32-38: run instructions (procedural steps, simple list is fine)
- Lines 40-41: exit code description (single-sentence explanations, simple list is fine)
- **Analysis:** No term/description pattern detected. Simple lists are appropriate. No issue.
30. **Meson build style**
- **Location:** `examples/wycheproof_validation/meson.build`
- **Issue:**
More information about the test-report
mailing list