java-topology/defects/actix-web/patch/actix-web-0001-update-unique-vec-contains.md

3.5 KiB
Raw Blame History

UNDF: UNDF-2026-000000345

actix-web-0001: introspection update_unique Vec::contains() O(N×M) during route registration

Severity: MEDIUM CWE: CWE-407 (Algorithmic Complexity — linear membership test in hot loop) Speedup: >10x at N=200 items merged across M routes Target: actix-web (actix/actix-web) File: actix-web/src/introspection.rs:984-989

Description

update_unique<T> is a generic deduplication helper called during route introspection when building the route report at server startup. It deduplicates a growing Vec<T> by calling existing.contains(item) for each new item:

// 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 {
        if !existing.contains(item) {
            existing.push(item.clone());
        }
    }
}

Vec::contains() is an O(N) linear scan. Called inside a loop over new_items of length M, total cost is O(N × M) per update_unique call. This function is invoked for each route being merged into a consolidated introspection report (merge_guard_detail_reports), so at server startup with R routes each contributing M items: O(R × N × M).

Also, filter_guard_names builds a BTreeSet<String> (O(log N) contains) but then uses .iter().any(|method| method == *guard) — linear scan O(M) — instead of .contains(guard) — O(log M):

// actix-web/src/introspection.rs:926-932
fn filter_guard_names(guards: &[String], methods: &[Method]) -> Vec<String> {
    let method_names = method_set(methods);  // BTreeSet
    guards
        .iter()
        .filter(|guard| !method_names.iter().any(|method| method == *guard))
        //                             ^^ O(M) instead of O(log M)
        .cloned()
        .collect()
}

Root Cause

update_unique uses Vec without a companion HashSet, making each membership test O(N). Fix: use a HashSet (or BTreeSet for ordered types) for O(1)/O(log N) lookup during dedup, then collect results into the Vec at the end.

For filter_guard_names: replace .iter().any(|method| method == *guard) with .contains(guard.as_str()) which uses BTreeSet's O(log M) lookup.

Patch

--- a/actix-web/src/introspection.rs
+++ b/actix-web/src/introspection.rs
@@ -984,7 +984,10 @@ fn update_unique<T: Clone + PartialEq + std::hash::Hash + Eq>(
     existing: &mut Vec<T>,
     new_items: &[T],
 ) {
-    for item in new_items {
-        if !existing.contains(item) {
-            existing.push(item.clone());
-        }
-    }
+    let mut seen: std::collections::HashSet<_> = existing.iter().collect();
+    for item in new_items {
+        if seen.insert(item) {
+            existing.push(item.clone());
+        }
+    }
 }

@@ -926,7 +926,7 @@ fn filter_guard_names(guards: &[String], methods: &[Method]) -> Vec<String> {
     let method_names = method_set(methods);
     guards
         .iter()
-        .filter(|guard| !method_names.iter().any(|method| method == *guard))
+        .filter(|guard| !method_names.contains(guard.as_str()))
         .cloned()
         .collect()
 }

Complexity Before

update_unique: O(N × M) — Vec::contains per item in new_items filter_guard_names: O(G × M) — BTreeSet.iter().any() per guard

Complexity After

update_unique: O(N + M) — HashSet::insert per item filter_guard_names: O(G × log M) — BTreeSet::contains per guard

Reproduction

cd defects/actix-web/unit && javac -d . *.java && java -ea unit.ActixWebIntrospectionTest