wave6a/6b: elasticsearch/opensearch/solr/llvm-0004-5/linux-0005-6/gcc-0002/tokio/actix-web + love2d/raylib new defects

This commit is contained in:
russell@unturf.com 2026-03-27 15:45:14 -04:00
parent a4b0cf4edd
commit 3eebda37e1
38 changed files with 3651 additions and 0 deletions

View file

@ -0,0 +1,105 @@
# 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:
```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 {
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):
```rust
// 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
```diff
--- 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
```

View file

@ -0,0 +1,79 @@
# actix-web-0002: WebSocket handshake protocol negotiation O(R×P) per upgrade request
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — linear membership test in hot loop)
**Speedup:** >10x at R=20 client protocols × P=20 server protocols
**Target:** actix-web-actors (actix/actix-web)
**File:** `actix-web-actors/src/ws.rs:431-435`
## Description
The WebSocket handshake helper `handshake_with_protocols()` performs protocol
negotiation by iterating over all client-requested protocols and, for each,
checking whether the server supports it with a linear scan over the server's
supported protocol list:
```rust
// actix-web-actors/src/ws.rs:426-435
let protocol = req
.headers()
.get(&header::SEC_WEBSOCKET_PROTOCOL)
.and_then(|req_protocols| {
let req_protocols = req_protocols.to_str().ok()?;
req_protocols
.split(',')
.map(|req_p| req_p.trim())
.find(|req_p| protocols.iter().any(|p| p == req_p))
// ^^ O(P) scan per client protocol
});
```
`protocols` is `&[&str]` (a slice). `protocols.iter().any(|p| p == req_p)` is
O(P) per client protocol. With R client protocols and P server protocols:
total cost **O(R × P)** per WebSocket upgrade request.
This function is called on every WebSocket upgrade, which is a connection
establishment hot path. With many concurrent WebSocket upgrades (e.g., a
chat server with thousands of connections/second) and many supported protocols,
this degrades quadratically.
## Root Cause
`protocols` is passed as `&[&str]` and scanned linearly. Fix: at the start of
`handshake_with_protocols`, build a `HashSet<&str>` from the server protocols
so each client protocol check is O(1).
## Patch
```diff
--- a/actix-web-actors/src/ws.rs
+++ b/actix-web-actors/src/ws.rs
@@ -373,6 +373,7 @@ pub fn handshake_with_protocols(
req: &HttpRequest,
protocols: &[&str],
) -> Result<HttpResponseBuilder, HandshakeError> {
+ let protocols_set: std::collections::HashSet<&str> = protocols.iter().copied().collect();
// WebSocket accepts only GET
...
@@ -426,7 +427,7 @@ pub fn handshake_with_protocols(
req_protocols
.split(',')
.map(|req_p| req_p.trim())
- .find(|req_p| protocols.iter().any(|p| p == req_p))
+ .find(|req_p| protocols_set.contains(req_p))
});
```
## Complexity Before
Per WebSocket upgrade request: **O(R × P)** — linear scan over server protocols per client protocol
## Complexity After
Per WebSocket upgrade request: **O(P + R)** — O(P) to build HashSet, O(1) per client protocol check
## Reproduction
```
cd defects/actix-web/unit && javac -d . *.java && java -ea unit.ActixWebIntrospectionTest
```

View file

@ -0,0 +1,276 @@
package unit;
import java.util.*;
/**
* Unit test for actix-web-0001 and actix-web-0002: CWE-407 in actix-web.
*
* actix-web-0001 (MEDIUM):
* File: actix-web/src/introspection.rs:984-989
* Symbol: update_unique — Vec::contains() inside a for loop
* Defect: Deduplication of route introspection data uses Vec::contains()
* which is O(N) per item. For M new items merged into an existing
* Vec of size N: O(N × M) total.
* Fix: Build a HashSet from existing items; each insertion is O(1).
* Total: O(N + M).
*
* actix-web-0002 (MEDIUM):
* File: actix-web-actors/src/ws.rs:431-435
* Symbol: handshake_with_protocols — protocols.iter().any() in .find()
* Defect: WebSocket protocol negotiation: for each client-requested protocol
* (R total), scan the server's supported list (P total): O(R × P)
* per upgrade request.
* Fix: Pre-build HashSet<&str> from server protocols at handshake start.
* Each client protocol check is then O(1). Total: O(P + R).
*
* Modeled here in Java:
* Rust Vec::contains() ≡ List<String> linear scan (defective)
* Rust HashSet::contains() ≡ HashSet<String> lookup (fixed)
* Comparison counts tracked at the membership-test site.
*
* Expected at N=M=200 (actix-web-0001):
* defective comparisons ≈ N × M = 40,000
* fixed comparisons ≈ N + M = 400
* ratio > 50×
*
* Expected at R=P=50 (actix-web-0002):
* defective comparisons = R × P = 2,500
* fixed comparisons = R = 50
* ratio = 50×
*/
public class ActixWebIntrospectionTest {
// =========================================================================
// actix-web-0001 model: update_unique with Vec vs HashSet
// =========================================================================
/**
* Defective update_unique: uses List.contains() (O(N) scan) inside loop.
* comparisons counts every element examined in contains().
*/
static long updateUniqueDefective(List<String> existing, List<String> newItems,
long[] comparisons) {
for (String item : newItems) {
// existing.contains(item) — O(existing.size()) linear scan
boolean found = false;
for (String x : existing) {
comparisons[0]++;
if (x.equals(item)) { found = true; break; }
}
if (!found) existing.add(item);
}
return comparisons[0];
}
/**
* Fixed update_unique: builds a HashSet from existing, then inserts each
* new item with O(1) HashSet.add (which returns false if already present).
* comparisons counts one hash-probe per item.
*/
static long updateUniqueFixed(List<String> existing, List<String> newItems,
long[] comparisons) {
Set<String> seen = new HashSet<>(existing);
for (String item : newItems) {
comparisons[0]++; // O(1) HashSet.add
if (seen.add(item)) {
existing.add(item);
}
}
return comparisons[0];
}
// =========================================================================
// actix-web-0002 model: WebSocket protocol negotiation, slice vs HashSet
// =========================================================================
/**
* Defective protocol negotiation: for each client protocol, scan server
* protocols linearly (protocols.iter().any()).
* Returns the first matched protocol, or null.
* comparisons counts every element examined in the inner any() scan.
*/
static String negotiateDefective(List<String> clientProtocols,
List<String> serverProtocols,
long[] comparisons) {
for (String req : clientProtocols) {
// protocols.iter().any(|p| p == req_p) — O(P) scan
for (String srv : serverProtocols) {
comparisons[0]++;
if (srv.equals(req)) {
return req; // first match wins
}
}
}
return null;
}
/**
* Fixed protocol negotiation: pre-build HashSet from server protocols,
* then check each client protocol with O(1) lookup.
* comparisons counts one hash-probe per client protocol.
*/
static String negotiateFixed(List<String> clientProtocols,
List<String> serverProtocols,
long[] comparisons) {
// Build HashSet once: O(P)
Set<String> serverSet = new HashSet<>(serverProtocols);
for (String req : clientProtocols) {
comparisons[0]++; // O(1) HashSet.contains
if (serverSet.contains(req)) {
return req;
}
}
return null;
}
// =========================================================================
// Tests
// =========================================================================
/**
* Test 1 — Correctness: defective and fixed update_unique produce same result.
*
* Existing: 50 unique strings. New: 50 strings (25 overlap + 25 novel).
*/
static void testUpdateUniqueCorrectnessMatch() {
List<String> existing1 = new ArrayList<>();
List<String> existing2 = new ArrayList<>();
List<String> newItems = new ArrayList<>();
for (int i = 0; i < 50; i++) {
existing1.add("item-" + i);
existing2.add("item-" + i);
}
// 25 overlap (already in existing) + 25 novel
for (int i = 25; i < 75; i++) newItems.add("item-" + i);
long[] c1 = {0}, c2 = {0};
updateUniqueDefective(existing1, newItems, c1);
updateUniqueFixed(existing2, newItems, c2);
assert new HashSet<>(existing1).equals(new HashSet<>(existing2))
: "update_unique results differ";
assert existing1.size() == 75
: "expected 75 items (50 original + 25 novel); got " + existing1.size();
System.out.println("PASS testUpdateUniqueCorrectnessMatch");
}
/**
* Test 2 — actix-web-0001: update_unique ratio at N=M=200.
*
* 200 existing items, 200 new unique items (no overlap — worst case for scan).
* Defective: sum of scan sizes = 200 + 201 + ... + 399 ≈ O(N×M).
* Fixed: 200 HashSet probes = O(M).
* Ratio > 50×.
*/
static void testUpdateUniqueRatioAtScale() {
int N = 200, M = 200;
List<String> existing1 = new ArrayList<>();
List<String> existing2 = new ArrayList<>();
for (int i = 0; i < N; i++) {
existing1.add("e" + i);
existing2.add("e" + i);
}
// New items are all unique (no overlap) — worst case
List<String> newItems = new ArrayList<>();
for (int i = 0; i < M; i++) newItems.add("n" + i);
long[] defComp = {0}, fixComp = {0};
updateUniqueDefective(existing1, newItems, defComp);
updateUniqueFixed(existing2, newItems, fixComp);
assert new HashSet<>(existing1).equals(new HashSet<>(existing2))
: "result mismatch";
// Defective: scan sizes are N, N+1, ..., N+M-1 (each novel item extends list)
long expectedDef = (long) N * M + (long) M * (M - 1) / 2;
assert defComp[0] == expectedDef
: "defective comparisons should be " + expectedDef + "; got " + defComp[0];
// Fixed: M probes
assert fixComp[0] == M
: "fixed comparisons should be M=" + M + "; got " + fixComp[0];
double ratio = (double) defComp[0] / fixComp[0];
assert ratio > 50.0
: "ratio should be >50× at N=M=200; got " + ratio;
System.out.printf(
"PASS testUpdateUniqueRatioAtScale (N=%d M=%d defective=%d fixed=%d ratio=%.1fx)%n",
N, M, defComp[0], fixComp[0], ratio);
}
/**
* Test 3 — actix-web-0002 Correctness: both negotiators find same protocol.
*
* Client requests ["graphql-ws", "chat", "json-patch"].
* Server supports ["json-patch", "graphql-ws"].
* First match should be "graphql-ws" (first in client list that server knows).
*/
static void testNegotiateCorrectnessMatch() {
List<String> client = Arrays.asList("graphql-ws", "chat", "json-patch");
List<String> server = Arrays.asList("json-patch", "graphql-ws");
long[] c1 = {0}, c2 = {0};
String defResult = negotiateDefective(client, server, c1);
String fixResult = negotiateFixed(client, server, c2);
assert "graphql-ws".equals(defResult)
: "defective should find 'graphql-ws'; got " + defResult;
assert defResult.equals(fixResult)
: "negotiation result mismatch: defective=" + defResult + " fixed=" + fixResult;
System.out.println("PASS testNegotiateCorrectnessMatch");
}
/**
* Test 4 — actix-web-0002: protocol negotiation ratio at R=P=50, no match (worst case).
*
* No overlap between client and server protocols — full scan required.
* Defective: R × P = 50 × 50 = 2,500 comparisons.
* Fixed: R = 50 comparisons (one HashSet probe per client protocol).
* Ratio = 50×.
*/
static void testNegotiateRatioAtScale() {
int R = 50, P = 50;
List<String> client = new ArrayList<>();
List<String> server = new ArrayList<>();
// No overlap: client protocols start with "c-", server with "s-"
for (int i = 0; i < R; i++) client.add("c-" + i);
for (int i = 0; i < P; i++) server.add("s-" + i);
long[] defComp = {0}, fixComp = {0};
String defResult = negotiateDefective(client, server, defComp);
String fixResult = negotiateFixed(client, server, fixComp);
assert defResult == null : "defective should return null (no match); got " + defResult;
assert fixResult == null : "fixed should return null (no match); got " + fixResult;
long expectedDef = (long) R * P;
assert defComp[0] == expectedDef
: "defective comparisons should be R*P=" + expectedDef + "; got " + defComp[0];
assert fixComp[0] == R
: "fixed comparisons should be R=" + R + "; got " + fixComp[0];
double ratio = (double) defComp[0] / fixComp[0];
assert ratio == P
: "ratio should be P=" + P + "; got " + ratio;
System.out.printf(
"PASS testNegotiateRatioAtScale (R=%d P=%d defective=%d fixed=%d ratio=%.1fx)%n",
R, P, defComp[0], fixComp[0], ratio);
}
// =========================================================================
public static void main(String[] args) {
testUpdateUniqueCorrectnessMatch();
testUpdateUniqueRatioAtScale();
testNegotiateCorrectnessMatch();
testNegotiateRatioAtScale();
System.out.println("4/4 PASS");
}
}