java-topology/defects/actix/patch/actix-web-0003-introspection-update-unique.md

103 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000000344
# actix-0001 — CWE-407: introspection `update_unique` O(R×G) Vec linear dedup
**Project:** actix-web
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Excessive Iteration)
**File:** `actix-web/src/introspection.rs`
**Lines:** 984989 (defect); also 935944 (`merge_guard_reports`)
**Feature gate:** `experimental-introspection`
---
## Defective Code
```rust
// actix-web/src/introspection.rs:984-989
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
for item in new_items { // outer: O(N) over new_items
if !existing.contains(item) { // inner: O(E) linear scan each time
existing.push(item.clone());
}
}
}
```
Called at registration time (lines 389392, 451454) for every route sharing a path prefix:
```rust
update_unique(&mut d.methods, &info.methods);
update_unique(&mut d.guards, &info.guards); // guards Vec grows unbounded
merge_guard_reports(&mut d.guard_details, &info.guard_details);
update_unique(&mut d.patterns, &info.patterns); // patterns Vec grows unbounded
```
`merge_guard_reports` (lines 935944) has the same shape — O(I×E) `iter_mut().find()` over a `Vec<GuardReport>`:
```rust
fn merge_guard_reports(existing: &mut Vec<GuardReport>, incoming: &[GuardReport]) {
for report in incoming {
if let Some(existing_report) = existing.iter_mut().find(|r| r.name == report.name) {
// …
}
}
}
```
---
## Complexity
| Dimension | Defective | Fixed |
|-----------|-----------|-------|
| `update_unique` over G guards across R registrations | O(R × G) | O(R + G) |
| `merge_guard_reports` over I incoming × E existing reports | O(I × E) | O(I + E) |
| `update_unique` inside `merge_guard_detail_reports` (headers/methods) | O(N²) | O(N) |
In a large app with R = 500 routes sharing a scope prefix, G = 50 accumulated guard names:
- Defective: 500 × 50 = 25,000 `.contains()` comparisons per finalization
- At G = 500: 250,000 comparisons (500x op-count ratio)
---
## Trigger Conditions
1. Application uses `experimental-introspection` feature.
2. Multiple routes register under the same path prefix (scope nesting).
3. Many distinct custom guard names accumulate per path (user-defined guards via `guard::fn_guard`).
4. Large microservice with hundreds of scoped routes (e.g., REST API with 500+ routes in nested scopes).
---
## Fix Description
Pre-build a `HashSet` for O(1) membership checks before the loop:
```rust
fn update_unique<T: Clone + PartialEq + Eq + std::hash::Hash>(
existing: &mut Vec<T>,
new_items: &[T],
) {
let seen: std::collections::HashSet<_> = existing.iter().collect();
for item in new_items {
if !seen.contains(item) {
existing.push(item.clone());
}
}
}
```
For `merge_guard_reports`, build a `HashMap<&str, usize>` (name → index into `existing`) before iterating `incoming`.
For types that cannot be hashed (e.g., `GuardDetailReport`), sort and binary-search, or use a two-pass approach.
---
## Speedup Estimate
At R = 500 routes, G = 500 unique guards accumulated:
- Defective: O(500 × 500) = 250,000 contains-checks
- Fixed: O(500) HashSet inserts + O(500) lookups = ~1,000 ops
- **Ratio: ~250x**
Measured in unit test: see `defects/actix/unit/ActixUpdateUniqueAlgorithm.java`.