whitepaper: 312 sites / 151 ecosystems — wave2+3 defect tables and PDF rebuild

Add 88 new defect entries to HIGH and MEDIUM tables:
  HIGH: mysql-0001/0002, mariadb-0001, redis-0001/0002, valkey-0001/0002, openvpn-0001,
        vlc-0001, prometheus-0001, otel-collector-0001, cockroachdb-0001..0004,
        tidb-0001..0008, kubernetes-0001/0002, go-0001, kotlin-0002, scala-0001,
        allegro5-0001, sdl2-0001, grafana-0001, clickhouse-0001, duckdb-0001,
        mongodb-0001, envoy-0001, istio-0001, cilium-0001, linkerd2-0001,
        linux-0001/0002/0003, tor-0002/0003, curl-0001, julia-0001, lua-0001,
        perl5-0001, nats-0001, spring-0003/0004, tomcat-0001, onos-0002, odl-0002

  MEDIUM: helm-0001, mariadb-0002, openssl-0001/0002, memcached-0001,
          cassandra-0001..0004, flink-0001, storm-0001/0002, zookeeper-0001..0003,
          pip-0001, gradle-0001, nginx-0001, haproxy-0001, caddy-0001, varnish-0001,
          ffmpeg-0001, gstreamer-0001, raylib-0001, love2d-0001, php-0001/0002,
          r-source-0001, cpython-0002, ruby-0001, rabbitmq-0003/0004, activemq-0001,
          ovs-0001, onos-0003, odl-0002, jetty-0001

PDF: 976K
This commit is contained in:
russell@unturf.com 2026-03-27 15:23:43 -04:00
parent b3842ab6b8
commit 9934133dcf
260 changed files with 18278 additions and 15 deletions

View file

@ -0,0 +1,66 @@
# activemq-0001 — Topic.addSubscription: O(n) CopyOnWriteArrayList.contains per subscriber
**Target:** apache/activemq
**File:** `activemq-broker/src/main/java/org/apache/activemq/broker/region/Topic.java`
**Function:** `addSubscription(ConnectionContext, Subscription)`
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Linear Membership Test in Subscribe Loop)
**Status:** PATCHED
## Defect
`consumers` is declared as:
```java
protected final CopyOnWriteArrayList<Subscription> consumers = new CopyOnWriteArrayList<Subscription>();
```
On every non-durable subscription add, the broker checks for duplicates:
```java
synchronized (consumers) {
if (!consumers.contains(sub)){
consumers.add(sub);
}
}
```
`CopyOnWriteArrayList.contains()` is O(n) — it iterates the entire array. With N concurrent
subscribers on a single topic, each new subscriber triggers an O(N) scan. In fan-out workloads
with many consumers (e.g., event bus topics with hundreds of subscribers), this degrades as
**O(N²)** across all subscription events over the lifetime of the topic.
The pattern appears three times in `addSubscription` (lines 151, 167, 293).
## Fix
Maintain a parallel `Set<Subscription>` (e.g., `Collections.newSetFromMap(new ConcurrentHashMap<>())`)
for O(1) membership checks. The `CopyOnWriteArrayList` is retained for ordered iteration during
dispatch. On add, check the set first; on remove, update both structures.
```java
private final Set<Subscription> consumerSet =
Collections.newSetFromMap(new ConcurrentHashMap<Subscription, Boolean>());
// In addSubscription:
synchronized (consumers) {
if (consumerSet.add(sub)) {
consumers.add(sub);
}
}
// In removeSubscription / deactivate:
synchronized (consumers) {
if (consumers.remove(sub)) {
consumerSet.remove(sub);
}
}
```
## Patch
`defects/activemq/patch/activemq-0001-topic-consumer-set.patch`
## Unit Test
`defects/activemq/unit/ActiveMQTopicConsumerTest.java`

View file

@ -0,0 +1,54 @@
# allegro5-0001 — al_play_sample O(n) linear scan of sample pool per play call
## Status
PATCHED
## Severity
MEDIUM
## Target
allegro5 `addons/audio/kcm_sample.c`
## CWE
CWE-407: Algorithmic Complexity — Insufficient Complexity Reduction Before Algorithmic
Intensive Operation
## Description
`al_play_sample()` performs an O(n) linear scan of the `auto_samples` pool to find the
first free (not playing, not locked) slot. The pool is sized by `al_reserve_samples(n)`.
```c
// kcm_sample.c:355
bool al_play_sample(ALLEGRO_SAMPLE *spl, float gain, float pan, float speed,
ALLEGRO_PLAYMODE loop, ALLEGRO_SAMPLE_ID *ret_id)
{
for (i = 0; i < _al_vector_size(&auto_samples); i++) { // O(n) per play
AUTO_SAMPLE *slot = _al_vector_ref(&auto_samples, i);
if (!al_get_sample_instance_playing(slot->instance) && !slot->locked) {
...
return true;
}
}
return false;
}
```
In typical game code calling `al_play_sample()` for every sound effect (footsteps,
gunshots, explosions) on each frame, with a pool of N reserved samples, each call
scans O(N) instances. With P play calls per frame the total cost is O(P * N).
## Fix
Maintain a `free_head` index or a separate queue/stack of free slot indices.
On slot release (sample finishes playing), push the index onto the free stack.
`al_play_sample` pops the free stack: O(1) amortized.
The existing `ALLEGRO_SAMPLE_ID` with `_index` already stores the slot index —
the infrastructure is in place to track free slots separately.
## Complexity
- Before: O(n) per al_play_sample call where n = al_reserve_samples count
- After: O(1) amortized with free-slot stack
## Files
- `defects/allegro5/patch/allegro5-0001.patch`
- `defects/allegro5/unit/Allegro5SamplePoolTest.java`

View file

@ -0,0 +1,60 @@
# caddy-0001: hostByHashing full pool scan per request
**Target:** caddy
**File:** `modules/caddyhttp/reverseproxy/selectionpolicies.go`
**Function:** `hostByHashing`
**Lines:** 842–851
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
Caddy's hash-based upstream selection policies (URI hash, header hash, cookie
hash, query hash, client-IP hash) all funnel through `hostByHashing`, which
iterates over every upstream in the pool, hashing each one with xxhash:
```go
for _, up := range pool {
if !up.Available() {
continue
}
h := hash(up.String() + s) // xxhash per upstream per request
if h > highestHash {
highestHash = h
upstream = up
}
}
```
With N upstreams and R requests/second, total xxhash calls = O(N × R). Each
xxhash call allocates a new `xxhash.New()` hasher (line 856–859).
The pool is stable between config reloads; the available subset changes only
when health checks flip. The per-request rehash of stable upstreams is wasted
work.
## Impact
- 50 upstreams × 50 000 RPS = 2 500 000 xxhash calls/second with allocations.
- Available-pool filtering (`!up.Available()`) still requires touching every
upstream struct, causing cache-line pressure.
## Fix
Cache the xxhash of each upstream's string at provision time
(`up.String()` is stable). Store `upstreamHash uint64` on the `Upstream` struct.
At request time: `h = cachedHash XOR hash(s)` — one hash call per request
regardless of pool size. Recompute the combined hash only when pool membership
changes (config reload).
For `Available()` filtering: maintain an atomic slice of available-upstream
indices updated by the health-check goroutine. Selection scans only available
entries.
## Patch
See `defects/caddy/patch/caddy-0001.patch`
## Unit Test
See `defects/caddy/unit/CaddyHostByHashingAlgorithmTest.java`

View file

@ -0,0 +1,60 @@
# cassandra-0001: DEAD_STATES/SILENT_SHUTDOWN_STATES List.contains() — O(N) per gossip round
**Severity:** MEDIUM
**File:** src/java/org/apache/cassandra/gms/Gossiper.java
**Line:** 147–151, 1334, 1343
**Status:** PATCHED
## Description
`Gossiper` declares two static collections of status strings used to classify
node liveness on every gossip round:
```java
static final List<String> DEAD_STATES = Arrays.asList(
REMOVING_TOKEN, REMOVED_TOKEN, STATUS_LEFT, HIBERNATE);
static ArrayList<String> SILENT_SHUTDOWN_STATES = new ArrayList<>();
static {
SILENT_SHUTDOWN_STATES.addAll(DEAD_STATES);
}
```
Both are `List` types, making `List.contains()` an O(N) linear scan.
`isDeadState()` and `isSilentShutdownState()` each call `.contains()` against
these lists. Both methods are invoked for every endpoint in the cluster on every
gossip tick (once per second). In a 1000-node cluster this is 1000 linear scans
per second, every second. The DEAD_STATES list has 4 elements today but the
pattern will degrade further if more statuses are added.
The gossip tick itself is O(|endpoints|) — appending O(|DEAD_STATES|) per
endpoint makes the full tick O(|endpoints| × |DEAD_STATES|) where it should be
O(|endpoints|).
## Root Cause
`Arrays.asList()` returns a fixed-size `List`, not a `Set`. `SILENT_SHUTDOWN_STATES`
is an `ArrayList`. Both structures use O(N) linear membership tests.
## Fix
Replace with `Set<String>` (e.g. `ImmutableSet` which is already on the
classpath via Guava, or `EnumSet` is not applicable because the values are
`String` status tokens):
```java
static final Set<String> DEAD_STATES = ImmutableSet.of(
REMOVING_TOKEN, REMOVED_TOKEN, STATUS_LEFT, HIBERNATE);
static final Set<String> SILENT_SHUTDOWN_STATES;
static {
Set<String> s = new HashSet<>(DEAD_STATES);
// any additional silent states can be added here
SILENT_SHUTDOWN_STATES = Collections.unmodifiableSet(s);
}
```
`Set.contains()` is O(1) amortized, making each gossip tick O(|endpoints|).
## Speedup
~4x at current DEAD_STATES cardinality; O(1) asymptotically regardless of
future status additions. At 1000 nodes: ~4000 string comparisons/sec → ~1000.

View file

@ -0,0 +1,82 @@
# cilium-0001: Requirement.hasValue slices.Contains O(n) called per identity in selector cache → O(n²)
**Target:** cilium/cilium
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `pkg/k8s/slim/k8s/apis/labels/selector.go`
**Status:** PATCHED
## Description
`Requirement.hasValue(value string)` calls `slices.Contains(r.strValues, value)`
where `r.strValues` is a `[]string`. This is O(V) where V = number of values in
the requirement's value set (e.g. a `In` selector with many values).
`hasValue` is called from `Requirement.Matches()`, which is called from
`internalSelector.Matches()`, which is called from `selectorcache.go` inside a
loop over every security identity in the cache (`for nid, id := range c.ids`).
Total complexity: O(I × R × V) where I = identities, R = requirements per
selector, V = values per requirement. In large clusters with thousands of
endpoints and label selectors with many values (e.g. namespace In [ns1, ns2,
..., nsN]), this becomes quadratic.
| Location | Pattern |
|----------|---------|
| `selector.go:217` | `slices.Contains(r.strValues, value)` — O(V) membership test |
| `selector.go:238` | `r.hasValue(val)` in `Matches()` hot path |
| `selectorcache.go:118` | `sel.source.Matches(id.lbls)` — called per identity in namespace loop |
| `selectorcache.go:728` | `sel.source.Matches(identity.lbls)` — called per identity in full scan |
## Root cause
```go
// selector.go:160 — strValues stored as plain slice
type Requirement struct {
key string
operator selection.Operator
strValues []string // ← slice, O(n) membership
}
// selector.go:216 — O(V) scan on every label match check
func (r *Requirement) hasValue(value string) bool {
return slices.Contains(r.strValues, value)
}
```
Caller in `selectorcache.go:100` (selections — iterates all identities):
```go
for nid, id := range c.ids {
if sel.source.Matches(id.lbls) { // O(R × V) per identity
...
}
}
```
In a cluster with I=10,000 identities, R=3 requirements, V=50 values each:
1,500,000 string comparisons per selector evaluation.
## Fix
Change `strValues` from `[]string` to `sets.String` (or `map[string]struct{}`).
The `hasValue` call becomes an O(1) map lookup. The existing `Values()` accessor
already returns a `sets.String`, so the change is straightforward.
```go
// After fix
type Requirement struct {
key string
operator selection.Operator
strValues sets.String // O(1) lookup
strValueList []string // keep for ordering/serialization only
}
func (r *Requirement) hasValue(value string) bool {
return r.strValues.Has(value) // O(1)
}
```
## Ops/ns numbers (Java benchmark)
See `defects/cilium/unit/CiliumTest.java`.
At I=10,000 identities, V=50 values: slow ~500,000 ops, fast ~10,000 ops → >50× speedup.

View file

@ -0,0 +1,43 @@
# clickhouse-0001: ReplaceColumnTransformerNode::findReplacementExpression linear scan
**Target:** ClickHouse/ClickHouse
**File:** `src/Analyzer/ColumnTransformers.cpp`,
`src/Analyzer/ColumnTransformers.h`
**Severity:** MEDIUM
**Pattern:** CWE-407 — O(n) membership test inside loop → O(n²)
## Description
`ReplaceColumnTransformerNode` stores replacement column names in
`Names replacements_names` (`std::vector<std::string>`).
`findReplacementExpression(expression_name)` performs an O(n) `std::find`
scan over `replacements_names` to look up the index of a replacement.
This function is called from `QueryAnalyzer::resolveColumnsTransformers()`
inside a nested double loop:
```
for each column in matched_expression_nodes_with_names: // O(C)
for each transformer in getColumnTransformers().getNodes(): // O(T)
replace_transformer->findReplacementExpression(column_name); // O(R)
```
Total: **O(C × T × R)** — cubic for queries that use `SELECT * REPLACE (...)`.
In production schemas with wide tables (C = 200+) and compound REPLACE lists
(R = 20+), this becomes measurable.
## Fix
Add a `NameToIndexMap replacements_index` (`std::unordered_map<std::string, size_t>`)
alongside `replacements_names`. Populate it on construction; use it in
`findReplacementExpression` for O(1) lookup.
See `defects/clickhouse/patch/0001.patch`.
## Complexity
| Operation | Before | After |
|-----------|--------|-------|
| `findReplacementExpression` | O(R) | O(1) |
| `resolveColumnsTransformers` total | O(C·T·R) | O(C·T) |

View file

@ -0,0 +1,68 @@
# cockroachdb-0001: IndexesUsed.add() slices.Contains on growing slice — O(N²)
**Severity:** MEDIUM
**File:** pkg/sql/opt/exec/execbuilder/builder.go
**Line:** 206–216
**Status:** PATCHED
## Description
`IndexesUsed` accumulates the set of (tableID, indexID) pairs referenced by a
query. Its `add` method deduplicates by scanning the existing slice:
```go
func (iu *IndexesUsed) add(tableID, indexID cat.StableID) {
s := struct{ tableID cat.StableID; indexID cat.StableID }{tableID, indexID}
if !slices.Contains(iu.indexes, s) {
iu.indexes = append(iu.indexes, s)
}
}
```
`add` is called once per index reference during execution plan building
(`relational.go` lines 859, 1009, 2475, 2999, 3197, 3339×2, 4123, 4170).
A query joining N tables each with multiple indexes calls `add` O(N) times;
each call scans the slice from position 0, giving O(N²) total comparisons.
For a 200-table star-schema query this is ~20 000 struct comparisons where a
map lookup would be ~200.
## Root Cause
`IndexesUsed` uses `[]struct{...}` as its backing store. `slices.Contains`
performs a linear scan. No map or set is used to track membership separately.
## Fix
Replace the slice with a `map` for O(1) deduplication; keep the slice for
ordered iteration in `Strings()`:
```go
type IndexesUsed struct {
indexes []struct {
tableID cat.StableID
indexID cat.StableID
}
seen map[[2]cat.StableID]struct{}
}
func (iu *IndexesUsed) add(tableID, indexID cat.StableID) {
key := [2]cat.StableID{tableID, indexID}
if iu.seen == nil {
iu.seen = make(map[[2]cat.StableID]struct{})
}
if _, ok := iu.seen[key]; !ok {
iu.seen[key] = struct{}{}
iu.indexes = append(iu.indexes, struct {
tableID cat.StableID
indexID cat.StableID
}{tableID, indexID})
}
}
```
`Strings()` is unchanged — it still iterates `iu.indexes`.
## Speedup
O(N²) → O(N). ~100x at N=200 indexes per complex query.

View file

@ -0,0 +1,64 @@
# cpython-0001 — pkgutil extend_path: O(n²) list membership inside portion loop
| Field | Value |
|-------|-------|
| ID | cpython-0001 |
| Target | CPython |
| File | `Lib/pkgutil.py` |
| Lines | 332–336 |
| CWE | CWE-407 (Algorithmic Complexity) |
| Severity | MEDIUM |
| Status | PATCHED |
## Description
`pkgutil.extend_path()` accumulates namespace package path portions while
deduplicating against a growing list. For each `portion` yielded by every
finder on the meta-path, the guard:
```python
for portion in portions:
if portion not in path: # O(n) list scan
path.append(portion) # n grows with each append
```
`path` is a plain `list`; Python's `list.__contains__` is O(n). As `n`
portions accumulate the total cost is O(n²). In environments with many
namespace packages (scientific stacks, monorepos, editable installs) the
meta-path can expose hundreds of portions, making startup/import time
super-linear.
## Reproduction
```python
import sys, pkgutil
# Synthetic: 500 distinct portions
portions = [f"/opt/pkg{i}/ns" for i in range(500)]
path = []
for p in portions:
if p not in path: # O(i) each iteration
path.append(p)
# 500 iterations × average 250 comparisons = 125,000 string comparisons
# vs set: 500 × O(1) = 500 hash probes
```
## Fix
Replace the list with a parallel `set` for O(1) membership testing while
preserving insertion order in the list.
```python
seen = set(path)
for portion in portions:
if portion not in seen:
path.append(portion)
seen.add(portion)
```
## Complexity
| Metric | Before | After |
|--------|--------|-------|
| Per-extend | O(n²) | O(n) |
| 500 portions | ~125 000 comparisons | ~500 hash probes |
| Speedup (500) | 1× | ~250× |

View file

@ -0,0 +1,55 @@
# curl-0001: Curl_cookie_add replace_existing — O(C²) linked-list scan per same-domain cookie
**Target:** curl
**File:** `lib/cookie.c`
**Function:** `replace_existing` (called from `Curl_cookie_add`)
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`Curl_cookie_add()` stores cookies in 63 hash buckets keyed by the top-level
domain (`COOKIE_HASH_SIZE = 63`). Before inserting a new cookie it calls
`replace_existing()`, which walks the entire linked list of the target bucket to
find and remove a matching (name, domain, path) cookie:
```c
// cookie.c line 831-832 (replace_existing)
size_t myhash = cookiehash(co->domain);
for(n = Curl_llist_head(&ci->cookielist[myhash]); n; n = Curl_node_next(n)) {
struct Cookie *clist = Curl_node_elem(n);
if(!strcmp(clist->name, co->name)) { // O(C/63) per call
...
}
}
```
Because the bucket size is fixed at 63, all cookies from the same domain land in
the same bucket. For C cookies sharing one domain the cost per `Curl_cookie_add`
call is O(C) — scanning the entire bucket. Over C insertions that is **O(C²)**.
Real-world exposure: a response that sets many cookies for one domain (e.g.
large JAR files, test harnesses, or adversarial servers) triggers quadratic
work in the client. A site setting 10 000 per-path cookies (within
`MAX_COOKIE_SEND_AMOUNT` limits) costs ~50 M string comparisons.
## Fix
Maintain a secondary `Curl_hash` keyed by `name` within `CookieInfo`, mapping
`name → Curl_llist_node *`. Before walking the bucket, look up the candidate
node in O(1); walk only if the hash indicates a potential match. Alternatively,
switch the per-bucket structure from a linked list to a hash table keyed by
`(name, domain, path)`.
Minimal patch: add a `Curl_hash name_index` field to `CookieInfo`, populated at
insert time and invalidated at remove time, so that `replace_existing` performs
a single hash lookup instead of a full list scan.
## Patch
`defects/curl/patch/curl-0001-cookie-name-hash-index.patch`
## Unit Test
`defects/curl/unit/CurlCookieReplaceTest.java`

View file

@ -0,0 +1,60 @@
# duckdb-0001: CorrelatedColumns dedup via vector linear scan in Binder
**Target:** duckdb/duckdb
**File:** `src/planner/binder.cpp`,
`src/planner/expression_binder/lateral_binder.cpp`
**Severity:** MEDIUM
**Pattern:** CWE-407 — O(n) membership test inside loop → O(n²)
## Description
`CorrelatedColumns` (in `src/include/duckdb/planner/binder.hpp`) is a wrapper
around `vector<CorrelatedColumnInfo>`. Two dedup sites perform O(n) linear
`std::find` membership tests on this vector before inserting a new entry:
### Site 1 — `Binder::AddCorrelatedColumn` (binder.cpp)
```cpp
void Binder::MergeCorrelatedColumns(CorrelatedColumns &other) {
for (idx_t i = 0; i < other.size(); i++) { // O(n) outer
AddCorrelatedColumn(other[i]); // O(n) inner std::find
}
}
void Binder::AddCorrelatedColumn(const CorrelatedColumnInfo &info) {
if (std::find(correlated_columns.begin(), correlated_columns.end(), info)
== correlated_columns.end()) { // O(n) scan
correlated_columns.AddColumn(info);
}
}
```
`MergeCorrelatedColumns` is called when flattening dependent joins.
For a query with C correlated columns across S subquery levels, total
work is **O(C² × S)**.
### Site 2 — `LateralBinder::ExtractCorrelatedColumns` (lateral_binder.cpp)
Called recursively for every expression node, with an O(n) `std::find` per
`BOUND_COLUMN_REF` encountered. For a wide SELECT with many lateral references
this is **O(expr_nodes × correlated_columns)**.
### Site 3 — `HasCorrelatedExpressions::VisitReplace` (has_correlated_expressions.cpp)
Inner loop: `for each correlated_column → std::find on subquery binder's
correlated_columns` — O(C²) per subquery expression visit.
## Fix
Add `unordered_set<ColumnBinding, ColumnBindingHash>` as a shadow set inside
`CorrelatedColumns` (or replace the vector with a set+ordered vector pair) so
that `contains()` is O(1) and insertion is O(1) amortized.
See `defects/duckdb/patch/0001.patch`.
## Complexity
| Operation | Before | After |
|-----------|--------|-------|
| `AddCorrelatedColumn` contains check | O(n) | O(1) |
| `MergeCorrelatedColumns` | O(n²) | O(n) |
| `ExtractCorrelatedColumns` per node | O(n) | O(1) |

View file

@ -0,0 +1,77 @@
# envoy-0001: PreviousHostsRetryPredicate std::find O(n) per retry attempt → O(n²)
**Target:** envoyproxy/envoy
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `source/extensions/retry/host/previous_hosts/previous_hosts.h`
**Status:** PATCHED
## Description
`PreviousHostsRetryPredicate::shouldSelectAnotherHost()` performs `std::find` on
`attempted_hosts_` (a `std::vector<HostDescription const*>`). This method is
called by the load balancer inside a host-selection retry loop
(`ZoneAwareLoadBalancerBase::chooseHost`) for every candidate host on every
retry attempt. With R retries and H previously-attempted hosts, the total work
is O(R × H) comparisons — O(n²) in the worst case where max_attempts equals
the cluster size.
| Location | Pattern |
|----------|---------|
| `previous_hosts.h:10` | `std::find(attempted_hosts_.begin(), attempted_hosts_.end(), &candidate_host)` |
| `load_balancer_impl.cc:668` | outer loop: `for (size_t i = 0; i < max_attempts; ++i)` calling `shouldSelectAnotherHost` |
| `thread_aware_lb_impl.cc:232` | same pattern in thread-aware variant |
## Root cause
```cpp
// previous_hosts.h — O(n) scan per call
bool shouldSelectAnotherHost(const Upstream::Host& candidate_host) override {
return std::find(attempted_hosts_.begin(), attempted_hosts_.end(),
&candidate_host) != attempted_hosts_.end();
}
void onHostAttempted(Upstream::HostDescriptionConstSharedPtr attempted_host) override {
attempted_hosts_.emplace_back(attempted_host.get());
}
private:
std::vector<Upstream::HostDescription const*> attempted_hosts_;
```
Caller loop in `load_balancer_impl.cc:658`:
```cpp
const size_t max_attempts = context ? context->hostSelectionRetryCount() + 1 : 1;
for (size_t i = 0; i < max_attempts; ++i) {
host = chooseHostOnce(context);
if (!host || !context || !context->shouldSelectAnotherHost(*host)) {
return host; // O(attempted_hosts) per iteration
}
}
```
With a cluster of N=100 endpoints and max_attempts=100, a single load-balancing
decision may require up to 100 × 100 = 10,000 pointer comparisons. In a busy
proxy handling thousands of requests/second, this compounds.
## Fix
Replace `attempted_hosts_` vector with `absl::flat_hash_set<>` or
`std::unordered_set<>`. Pointer identity is the key, so no custom hash is
needed — pointer addresses hash trivially.
```cpp
#include "absl/container/flat_hash_set.h"
bool shouldSelectAnotherHost(const Upstream::Host& candidate_host) override {
return attempted_hosts_.contains(&candidate_host); // O(1)
}
void onHostAttempted(Upstream::HostDescriptionConstSharedPtr attempted_host) override {
attempted_hosts_.insert(attempted_host.get()); // O(1) amortized
}
private:
absl::flat_hash_set<Upstream::HostDescription const*> attempted_hosts_;
```
## Ops/ns numbers (Java benchmark)
See `defects/envoy/unit/EnvoyTest.java`.
At R=100 retries, H=100 hosts: slow ~5050 ops, fast ~100 ops → >50× speedup.

View file

@ -0,0 +1,61 @@
# ffmpeg-0001 — Codec Tag Linear Scan Per Stream (CWE-407)
**Status:** PATCHED
**Severity:** MEDIUM
**Target:** FFmpeg `libavformat/utils.c`
**Functions:** `ff_codec_get_tag()`, `ff_codec_get_id()`, `av_codec_get_tag2()`
## Defect
`ff_codec_get_tag()` performs an O(N) linear scan over a static `AVCodecTag[]`
array to map a codec ID to a 4-byte FourCC tag. The arrays are large:
| Table | Entries |
|---------------------------|---------|
| `ff_codec_movvideo_tags` | 239 |
| `ff_codec_bmp_tags` | 460 |
| `ff_codec_movaudio_tags` | 60 |
| `ff_codec_wav_tags` | 85 |
This function is called for **every stream** in every muxer hot path:
- `movenc.c:2135` — `mov_get_codec_tag()` calls it 2–3 times per video/audio track
- `matroskaenc.c:1251,1262,1267,1281` — Matroska mux does the same
- `flvenc.c:260,1012,1296` — FLV encoder
- `cafenc.c:120,153` — CAF encoder
- `au.c:295` — AU encoder
When multiplexing a file with many streams (e.g., a playlist transcode or
broadcast ingest with 50+ streams), this becomes O(S × N) where S = stream
count and N = table size (up to 460).
`ff_codec_get_id()` is worse — it scans the array **twice** (once exact, once
case-insensitive via `ff_toupper4`) for a total of O(2N) per call.
## Root Cause
`AVCodecTag` arrays are statically allocated flat arrays with sentinel
`AV_CODEC_ID_NONE` terminators. No hash index is built at startup.
Both directions (id→tag and tag→id) are O(N) linear scans.
## Fix
Build a `uint32_t → AVCodecID` and `AVCodecID → uint32_t` hash map
at program startup (or lazily on first call) keyed on codec ID /
FourCC tag. The arrays are read-only after init; one-time O(N) build
cost, then O(1) lookups.
Patch: `defects/ffmpeg/patch/ffmpeg-0001.patch`
## Complexity
| | Before | After |
|---|---|---|
| `ff_codec_get_tag` | O(N), N≤460 | O(1) amortized |
| `ff_codec_get_id` | O(2N) | O(1) |
| `av_codec_get_tag2` | O(T×N) T=table count | O(1) |
## Benchmark
See `defects/ffmpeg/unit/FFmpegCodecTagTest.java` — at N=500 (synthetic),
the linear scan executes ≥100× more comparisons than the hash map.

View file

@ -0,0 +1,47 @@
# flink-0001: JobGraph user-jar dedup — List.contains() O(n²)
**Target:** Apache Flink
**File:** `flink-runtime/src/main/java/org/apache/flink/runtime/jobgraph/JobGraph.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`JobGraph.addJar(Path)` and `addUserJarBlobKey(PermanentBlobKey)` deduplicate entries
using `List.contains()` on `ArrayList` fields. Each call scans the entire list linearly.
When a job graph is constructed with N jar entries (via `addJars(List<URL>)`), total cost
is O(1) + O(2) + … + O(n) = **O(n²)**.
```java
// flink-runtime/.../JobGraph.java line 568
private final List<Path> userJars = new ArrayList<Path>(); // O(n) contains
private final List<PermanentBlobKey> userJarBlobKeys = new ArrayList<>(); // O(n) contains
public void addJar(Path jar) {
if (!userJars.contains(jar)) { // O(n) per call
userJars.add(jar);
}
}
public void addUserJarBlobKey(PermanentBlobKey key) {
if (!userJarBlobKeys.contains(key)) { // O(n) per call
userJarBlobKeys.add(key);
}
}
```
`addJars(List<URL>)` calls `addJar()` in a loop, making the total cost O(n²) for n jars.
## Fix
Replace `ArrayList` with `LinkedHashSet` (preserves insertion order, O(1) contains/add)
and expose a `List` view via `new ArrayList<>(set)` only at read time.
## Patch
`defects/flink/patch/flink-0001.patch`
## Unit Test
`defects/flink/unit/FlinkJobGraphJarDedupTest.java`

View file

@ -0,0 +1,63 @@
# go-0001 — CWE-407: isParameterized tparams slices.Index linear scan
**Target:** golang/go — `src/cmd/compile/internal/types2/infer.go`
**Severity:** MEDIUM
**Complexity:** O(n²) → O(n)
## Defect
`tpWalker.isParameterized` walks the type graph recursively. At each `*TypeParam`
node (line 630) it calls `slices.Index(w.tparams, t)` to check membership:
```go
case *TypeParam:
return slices.Index(w.tparams, t) >= 0
```
`w.tparams` is a `[]*TypeParam` slice. `slices.Index` is O(n) linear scan.
`isParameterized` is called in the inner body of type-inference loops (lines 173,
290, 420–443, 451). For a generic function with N type parameters, each call walks
up to N TypeParam nodes, each doing an O(N) scan → O(N²) total per inference
invocation. With deeply nested generic types or long type parameter lists, this
compounds across every call site in the outer `dirty` loop.
## Fix
Replace the `tparams []*TypeParam` slice with a `tset map[*TypeParam]bool`
pre-built before walking. Then membership is O(1) per node.
```go
func isParameterized(tparams []*TypeParam, typ Type) bool {
tset := make(map[*TypeParam]bool, len(tparams))
for _, tp := range tparams {
tset[tp] = true
}
w := tpWalker{
tparams: tset,
seen: make(map[Type]bool),
}
return w.isParameterized(typ)
}
type tpWalker struct {
tparams map[*TypeParam]bool // was []*TypeParam
seen map[Type]bool
}
// In isParameterized switch:
case *TypeParam:
return w.tparams[t]
```
## Impact
- Triggered during type inference for every generic function call
- Scales with number of type parameters × depth of type graph
- Projects with many generic functions (e.g. large generics-heavy codebases)
see O(n²) degradation in compile time
## Files
- Patch: `defects/go/patch/go-0001.patch`
- Unit: `defects/go/unit/GoInferTest.java`

View file

@ -0,0 +1,39 @@
# gradle-0001 — OptionReader: CollectionUtils.toList(optionNames).contains() rebuilt per method
## Target
`gradle/gradle` — `subprojects/core/src/main/java/org/gradle/api/internal/tasks/options/OptionReader.java`
## CWE
CWE-407: Algorithmic Complexity — O(n) linear membership inside a hot loop
## Location
```
OptionReader.java:94-96
for (JavaMethod<Object, ?> optionValueMethod : optionValueMethods) {
String[] optionNames = getOptionNames(optionValueMethod);
if (CollectionUtils.toList(optionNames).contains(optionElement.getOptionName())) {
```
## Description
`getOptionValueMethodForOption()` iterates over all `@OptionValues`-annotated methods in a task
class. For each method, it calls `CollectionUtils.toList(optionNames)` — converting a `String[]` to
a new `ArrayList` — then calls `.contains()` on it, performing a linear scan.
This runs during task option resolution: once per `(OptionElement, task class)` pair. For a task
with A option elements and M value-methods each with N option names, complexity is O(A × M × N).
The `optionNames` array is sourced from `@OptionValues(value = {...})` annotation data and is
effectively a constant per method — a perfect candidate for a precomputed `HashSet`.
## Complexity
- Slow path: O(A × M × N) — new ArrayList per inner-loop iteration
- Fast path: O(A × M) with `Arrays.asList(optionNames)` → `HashSet` or direct array membership check
## Patch
`defects/gradle/patch/gradle-0001-option-reader-set-membership.patch`
## Unit Test
`defects/gradle/unit/GradleOptionReaderTest.java`
## Status
PATCHED

View file

@ -0,0 +1,50 @@
# grafana-0001: dfs() visited-array Array.includes — O(n²) on time-range refresh
**Target:** Grafana (public/app/features/variables)
**File:** `public/app/features/variables/state/actions.ts`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`dfs()` and `getVariablesThatNeedRefreshNew()` track visited graph nodes using a
plain `string[]` array (`visitedDfs`). Each visit check calls `visited.includes(node.name)`
which is O(V) where V = visited count. The outer `allVariables.forEach` iterates N
variables, and inside `dfs()` every `outputEdges.forEach` call also performs
`!visited.includes(child.name)` — making the total cost **O(N²)** in the number of
dashboard variables.
This code runs on every time-range change event for every dashboard that uses
query variables with `onTimeRangeChanged` refresh. Dashboards with 50+ variables
(common in large Grafana deployments) experience quadratic work per time-range
update.
```typescript
// actions.ts line 666-671
const dfs = (node, visited: string[], variables, variablesRefreshTimeRange) => {
if (!visited.includes(node.name)) { // O(V) — called O(N) times → O(N²)
visited.push(node.name);
}
node.outputEdges.forEach((e) => {
if (child && !visited.includes(child.name)) { // another O(V) per edge
```
And the outer loop (line 728):
```typescript
if (visitedDfs.includes(v.name)) { // O(V) inside forEach over allVariables → O(N²)
```
## Fix
Replace `visited: string[]` with `visited: Set<string>`. All `.includes()` calls
become `.has()` — O(1). `.push()` becomes `.add()`. Total DFS cost drops to O(N + E)
where E = number of variable dependency edges.
## Patch
`defects/grafana/patch/grafana-0001.patch`
## Unit Test
`defects/grafana/unit/GrafanaTest.java`

View file

@ -0,0 +1,84 @@
# gstreamer-0001 — gst_element_factory_list_filter() O(N×M) Per Caps Change (CWE-407)
**Status:** PATCHED
**Severity:** HIGH
**Target:** GStreamer `gst/gstelementfactory.c`, `gst-plugins-base/gst/playback/gstdecodebin3.c`
**Functions:** `gst_element_factory_list_filter()`, `gst_registry_feature_filter()`
## Defect
`gst_element_factory_list_filter()` performs an O(N × M) scan where:
- N = number of registered element factories (can be 500–1000 with full plugin install)
- M = number of pad templates per factory (typically 1–4)
This is called from `create_decoder_factory_list()` (gstdecodebin3.c:3717)
on **every caps change** during autoplugging:
```c
static GList *
create_decoder_factory_list(GstDecodebin3 *dbin, GstCaps *caps)
{
gst_decode_bin_update_factories_list(dbin); // O(N) feature scan
res = gst_element_factory_list_filter( // O(N × M)
dbin->decoder_factories, caps, GST_PAD_SINK, TRUE);
}
```
`gst_decode_bin_update_factories_list()` itself calls
`gst_element_factory_list_get_elements()` → `gst_registry_feature_filter()`,
which snapshots the entire feature list (O(N) with lock), then applies a
callback to each entry.
### Inner loop complexity
```c
gst_element_factory_list_filter(GList *list, const GstCaps *caps, ...):
for each factory in list: // O(N)
for each pad_template in factory: // O(M)
tmpl_caps = gst_static_caps_get(...)
gst_caps_can_intersect(caps, tmpl_caps) // O(S1 × S2) caps structures
```
`gst_caps_can_intersect` is itself O(S1 × S2) where S1, S2 are the number of
structures in each caps. Total: O(N × M × S²).
### Hot path
`db_output_stream_setup_decoder()` (gstdecodebin3.c:3862) calls
`create_decoder_factory_list()` per output stream on caps negotiation. In a
multi-stream media file (e.g., MKV with 5 audio tracks + video + subtitles),
this fires for every stream setup and every dynamic caps renegotiation.
## Root Cause
The factory list has no caps-keyed index. Every decoder selection requires
a full scan. The registry does have a hash (`feature_hash`) for name lookup,
but no structure for caps-based lookup.
## Fix
Build a caps-structure-name → factory multimap at registry update time.
Each `GstStaticPadTemplate` has a `static_caps` with a media type string
(e.g., `"video/x-h264"`). Index on the media type (first structure name)
using a `GHashTable<gchar*, GList*>`. On `list_filter`, extract the
media type from `caps`, hash-lookup the candidate factories, and verify
full caps compatibility only against those candidates (typically 1–3).
Patch: `defects/gstreamer/patch/gstreamer-0001.patch`
## Complexity
| | Before | After |
|---|---|---|
| `list_filter` | O(N × M × S²) | O(k × M × S²), k≈1–3 |
| `get_elements` | O(N) full scan | O(N) once, then O(1) via hash |
| Per stream setup | O(N × M) | O(1) amortized |
With N=500, M=2, S=2: before=2000 caps ops per stream setup;
after=~12 caps ops (k=3). ~167× reduction.
## Benchmark
See `defects/gstreamer/unit/GstreamerFactoryFilterTest.java` — at N=500
factories, linear filter vs hash-indexed filter; linear executes >100×
more intersection tests.

View file

@ -0,0 +1,65 @@
# haproxy-0001: pat_match_bin list walk below LRU threshold
**Target:** haproxy
**File:** `src/pattern.c`
**Function:** `pat_match_bin` (also `pat_match_sub`, `pat_match_end`, `pat_match_beg`)
**Lines:** 559–588
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
HAProxy evaluates ACL patterns on every request. `pat_match_bin` walks a linked
list of binary patterns with `list_for_each_entry` on every call:
```c
list_for_each_entry(lst, &expr->patterns, list) {
pattern = &lst->pat;
if (pattern->ref->gen_id != expr->ref->curr_gen)
continue;
if (pattern->len != smp->data.u.str.data)
continue;
if (memcmp(pattern->ptr.str, smp->data.u.str.area,
smp->data.u.str.data) == 0) {
ret = pattern;
break;
}
}
```
The LRU cache (`pat_lru_tree`) only activates when `entry_cnt >= 20`. Below
that threshold — the common case for most HAProxy ACL files — every request
triggers a full O(P) list walk. With R requests/second and P patterns, total
work is O(P × R).
The same defect exists in `pat_match_sub`, `pat_match_end`, and `pat_match_beg`
(all use `list_for_each_entry` with no tree fallback and LRU threshold ≥ 20).
`pat_match_regm` has no LRU at all.
`pat_match_str` and `pat_match_beg` already have an `ebst_lookup` / EB-tree
fast path for exact/prefix matches but only when patterns were inserted into the
tree; the list fallback still executes for patterns not in the tree.
## Impact
- Every ACL binary-match expression with 1–19 patterns: O(P) per request.
- Production ACLs: 4–16 backend-selection rules, evaluated on every request.
- 10 000 RPS × 10 patterns = 100 000 list-node traversals/second.
## Fix
Lower the LRU activation threshold from 20 to 1 for `pat_match_bin`.
Alternatively, use a fixed-size hash map (key = length-prefixed binary value,
value = pattern pointer) built at configuration commit time, consulted O(1) at
request time.
## Patch
See `defects/haproxy/patch/haproxy-0001.patch`
## Unit Test
See `defects/haproxy/unit/HaproxyPatMatchBinAlgorithmTest.java`

View file

@ -0,0 +1,55 @@
# helm-0001: processDependencyEnabled — O(n²) nested dependency lookup
**Target:** helm/helm
**File:** `internal/chart/v3/util/dependencies.go`
**Severity:** MEDIUM
**Pattern:** CWE-407 — O(n) scan inside O(n) loop → O(n²)
## Description
`processDependencyEnabled` contains two O(n²) patterns:
### Pattern A — nested loop (lines 157-163)
```go
for _, existing := range c.Dependencies() { // O(E)
for _, req := range c.Metadata.Dependencies { // O(M) per existing
if existing.Name() == req.Name && ...
```
For each already-loaded chart it scans the full metadata dependency list to decide whether to
keep it. Complexity: O(E × M).
### Pattern B — getAliasDependency called per metadata dep (lines 166-176)
```go
for _, req := range c.Metadata.Dependencies { // O(M)
if chartDependency := getAliasDependency(c.Dependencies(), req) // O(C) each
```
`getAliasDependency` is itself a linear scan over `c.Dependencies()`. Complexity: O(M × C).
In a chart with D dependencies both patterns are O(D²). In umbrella charts or operator bundles
that embed dozens of sub-charts this shows up as O(D²) work on every `helm install` / `helm
upgrade`.
## Fix
Index `c.Dependencies()` by name before the loops:
```go
depsByName := make(map[string]*chart.Chart, len(c.Dependencies()))
for _, dep := range c.Dependencies() {
depsByName[dep.Name()] = dep
}
```
Then replace both inner scans with O(1) map lookups.
## Patch
`defects/helm/patch/helm-0001.patch`
## Unit test
`defects/helm/unit/HelmTest.java`

View file

@ -0,0 +1,78 @@
# istio-0001: virtualHostMatch slices.Contains(vh.Domains) O(n) inside VH×patch nested loop → O(n²)
**Target:** istio/istio
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `pilot/pkg/networking/core/envoyfilter/rc_patch.go`
**Status:** PATCHED
## Description
`virtualHostMatch()` calls `slices.Contains(vh.Domains, match.DomainName)` to
test whether a VirtualHost serves a given domain. This function is called from
`patchVirtualHost()`, which is invoked in a loop over every VirtualHost in a
route configuration. For each VirtualHost there is an inner loop over all
applicable EnvoyFilter patches. Each `slices.Contains` call is O(D) where D =
number of domains on the VirtualHost. The overall complexity is O(VH × P × D)
— cubic in the worst case.
In a large Istio mesh with many services and EnvoyFilter rules, VirtualHosts
routinely have dozens of domain aliases (service name, FQDN, short name,
wildcard variants). A single xDS push iterates this product for every
listener/route-config pair.
| Location | Pattern |
|----------|---------|
| `rc_patch.go:346` | `slices.Contains(vh.Domains, match.DomainName)` — O(D) membership test |
| `rc_patch.go:116` | `virtualHostMatch(virtualHosts[idx], rp)` called per VH per patch |
| `rc_patch.go:79` | outer loop: `for i := range routeConfiguration.VirtualHosts` |
## Root cause
```go
// rc_patch.go:327 — O(D) per call
func virtualHostMatch(vh *route.VirtualHost, rp *model.EnvoyFilterConfigPatchWrapper) bool {
match := rp.Match.GetRouteConfiguration().GetVhost()
if match == nil { return true }
return (match.Name == "" || match.Name == vh.Name) &&
(match.DomainName == "" || slices.Contains(vh.Domains, match.DomainName))
}
// rc_patch.go:112 — called in O(VH × P) loop
for i := range routeConfiguration.VirtualHosts {
...
virtualHostMatch(virtualHosts[idx], rp) // O(D) inside
```
With 500 VirtualHosts × 20 patches × 15 domains each: 150,000 string
comparisons per route-config push.
## Fix
Build a `map[string]*route.VirtualHost` keyed by domain before the patch loop,
so each domain lookup is O(1).
```go
// Build domain→VH index once, outside the patch loop
domainIndex := make(map[string]*route.VirtualHost, len(routeConfiguration.VirtualHosts))
for _, vh := range routeConfiguration.VirtualHosts {
for _, d := range vh.Domains {
domainIndex[d] = vh
}
}
// virtualHostMatch: O(1)
func virtualHostMatchFast(vh *route.VirtualHost, rp *model.EnvoyFilterConfigPatchWrapper,
domainIndex map[string]*route.VirtualHost) bool {
match := rp.Match.GetRouteConfiguration().GetVhost()
if match == nil { return true }
if match.Name != "" && match.Name != vh.Name { return false }
if match.DomainName == "" { return true }
return domainIndex[match.DomainName] == vh
}
```
## Ops/ns numbers (Java benchmark)
See `defects/istio/unit/IstioTest.java`.
At VH=500, P=20, D=15: slow ~150,000 ops, fast ~10,000 ops → >10× speedup.

View file

@ -0,0 +1,75 @@
# jetty-0001: HttpFields.formatCsvExcludingExisting — QuotedCSV.getValues() List.contains() O(n²)
**Target:** Eclipse Jetty
**File:** `jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`HttpFields.Mutable.addCSV()` (both header and name variants) calls the private
helper `formatCsvExcludingExisting(QuotedCSV existing, String... values)`. Inside
that helper the loop iterates over all incoming `values` and calls
`existing.getValues().contains(unquoted)` on each iteration.
`QuotedCSV.getValues()` returns the internal `_values` field, which is
`ArrayList<String>` (line 107). Each `.contains()` call is therefore O(m) where
m is the number of existing CSV values. With V values to add and M existing
values, the total is O(V × M).
When `addCSV()` is called per-request (e.g. to add `Vary` or `Cache-Control`
token lists), V and M grow with the number of directives/values, making this
O(n²) under adversarial or large header inputs.
```java
// HttpFields.java line 1576-1591
private static String formatCsvExcludingExisting(QuotedCSV existing, String... values) {
boolean add = true;
if (existing != null && !existing.isEmpty()) {
add = false;
for (int i = values.length; i-- > 0; ) {
String unquoted = QuotedCSV.unquote(values[i]);
if (existing.getValues().contains(unquoted)) // O(m) scan per value → O(V×M)
values[i] = null;
else
add = true;
}
}
...
}
// QuotedCSV.java line 107
private final List<String> _values = new ArrayList<>(); // O(n) contains()
```
## Fix
Convert the `existing` values to a `HashSet<String>` once before the loop, then
call `existingSet.contains(unquoted)` at O(1) per lookup.
```java
private static String formatCsvExcludingExisting(QuotedCSV existing, String... values) {
boolean add = true;
if (existing != null && !existing.isEmpty()) {
add = false;
Set<String> existingSet = new HashSet<>(existing.getValues()); // O(m) once
for (int i = values.length; i-- > 0; ) {
String unquoted = QuotedCSV.unquote(values[i]);
if (existingSet.contains(unquoted)) // O(1) per lookup
values[i] = null;
else
add = true;
}
}
...
}
```
## Patch
`defects/jetty/patch/jetty-0001.patch`
## Unit Test
`defects/jetty/unit/JettyHttpFieldsCsvTest.java`

View file

@ -0,0 +1,60 @@
# julia-0001: isrelocatable() scans Vector for membership per include → O(n²)
**Target:** JuliaLang/julia
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `base/loading.jl` — `isrelocatable()` (~line 2096)
**Status:** PATCHED
## Description
`isrelocatable()` reads the cache header to retrieve `includes` (all included
files) and `includes_srcfiles` (the subset that are source files). It then
loops over `includes` and tests `inc ∉ includes_srcfiles` to identify
include-dependencies. `includes_srcfiles` is a `Vector{CacheHeaderIncludes}`,
so `∉` compiles to a linear scan using `==` on each element. With `n` entries
in `includes` and up to `n` entries in `includes_srcfiles`, this is O(n²).
During package precompile validation, `isrelocatable()` is called for every
package in the depot. Large projects (hundreds of includes) pay quadratic cost
on every validation pass.
## Root cause
```julia
# base/loading.jl ~2102
_, (includes, includes_srcfiles, _), _... = _parse_cache_header(io, path)
for inc in includes # outer O(n)
if inc ∉ includes_srcfiles # inner O(n) Vector linear scan
track_content = inc.mtime == -1.0
track_content || return false
end
end
```
`includes_srcfiles` is a `Vector{CacheHeaderIncludes}` built in
`parse_cache_header`. The `∉` operator on a Vector is O(length).
## Fix
Build a `Set{CacheHeaderIncludes}` from `includes_srcfiles` before the loop so
membership tests are O(1).
```julia
srcfiles_set = Set{CacheHeaderIncludes}(includes_srcfiles)
for inc in includes
if inc ∉ srcfiles_set # O(1) hash lookup
track_content = inc.mtime == -1.0
track_content || return false
end
end
```
`CacheHeaderIncludes` is a mutable struct; equality (`==`) is already defined
structurally via `isequal` fallback, and `hash` can be derived from the
`filename` field (unique per include).
## Ops numbers (Java benchmark)
See `defects/julia/unit/JuliaTest.java` (bench label "isrelocatable-includes").
At N=1000 includes: slow ~500,500 comparisons, fast ~1000 → ~500× speedup.

View file

@ -0,0 +1,54 @@
# kotlin-0002 — CWE-407: TypeBoundsImpl bounds ArrayList.contains linear scan
**Target:** JetBrains/kotlin — `compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/inference/`
**Severity:** HIGH
**Complexity:** O(n²) → O(n)
## Defect
`TypeBoundsImpl.bounds` is declared as `ArrayList<Bound>` (TypeBoundsImpl.kt:31):
```kotlin
override val bounds = ArrayList<Bound>()
```
`ConstraintSystemBuilderImpl.addBound` (line 277) calls:
```kotlin
if (typeBounds.bounds.contains(bound)) return
```
`ArrayList.contains` is O(n) — it walks the list comparing each element.
`Bound` has correct `equals`/`hashCode` (TypeBounds.kt:49–70), so a hash-backed
collection would give O(1) membership. The fix is to use `LinkedHashSet<Bound>`
which preserves insertion order (needed for deterministic type inference output)
while providing O(1) `contains`.
`addBound` is the hot path in the classic Hindley-Milner constraint accumulation
loop: for every call argument, every type variable gets bounds added. With K
constraints per type variable and N type variables, the total cost of all
`contains` checks is O(K²×N) instead of O(K×N).
## Fix
```kotlin
// TypeBoundsImpl.kt line 31 — was ArrayList<Bound>
override val bounds: MutableSet<Bound> = LinkedHashSet()
```
`TypeBounds.bounds` interface declares `Collection<Bound>` so this is a
drop-in replacement. The `filter` call on line 84 (`bounds.filter { ... }`)
works on any `Collection`.
## Impact
- Every generic function call in Kotlin source triggers constraint accumulation
- Projects with large call graphs (ktor, coroutines, Arrow) experience O(n²) type
inference time proportional to the number of type constraints per call site
- The defect is in the classic constraint-based type inference inner loop
## Files
- Patch: `defects/kotlin/patch/kotlin-0002-typeboundsimpl-linkedhashset.patch`
- Unit: `defects/kotlin/unit/KotlinTypeBoundsTest.java`

View file

@ -0,0 +1,62 @@
# kubernetes-0001: Job pod failure policy — O(n) exit-code set membership per container per rule
**Target:** kubernetes/kubernetes
**File:** `pkg/controller/job/pod_failure_policy.go`
**Severity:** MEDIUM
**Pattern:** CWE-407 — linear membership test inside a loop
## Description
`isOnExitCodesOperatorMatching` calls `slices.Contains(requirement.Values, exitCode)` to check
whether a container exit code matches an `In` or `NotIn` policy rule. `requirement.Values` is an
unsorted `[]int32` slice; every call performs an O(V) linear scan.
This function is called from `getMatchingContainerFromList`, which iterates over every container
status in a pod — meaning every failing pod triggers O(C × R × V) work, where:
- C = number of containers in the pod
- R = number of `OnExitCodes` rules in the policy
- V = number of values in a rule's `Values` list
In large batch jobs with many containers and complex failure policies this degrades to O(n²) per
pod failure event processed by the job controller.
## Call chain
```
managedJob (job_controller.go)
→ matchPodFailurePolicy (pod_failure_policy.go:44)
→ matchOnExitCodes (pod_failure_policy.go:86)
→ getMatchingContainerFromList (pod_failure_policy.go:107)
→ isOnExitCodesOperatorMatching (pod_failure_policy.go:123)
→ slices.Contains(requirement.Values, exitCode) ← O(V) each call
```
## Slow path
```go
// O(V) per container per rule — called inside for _, containerStatus := range containerStatuses
func isOnExitCodesOperatorMatching(exitCode int32, requirement *...) bool {
switch requirement.Operator {
case ..OpIn:
return slices.Contains(requirement.Values, exitCode) // LINEAR
case ..OpNotIn:
return !slices.Contains(requirement.Values, exitCode) // LINEAR
}
}
```
## Fix
Sort `requirement.Values` once at admission time (already validated as non-empty) and use
`sort.SearchInt32s` / binary search at match time: O(log V). Alternatively, build a
`map[int32]struct{}` from `Values` once and reuse it throughout the rule evaluation — O(1) per
lookup, O(V) build amortised across all containers.
## Patch
`defects/kubernetes/patch/kubernetes-0001.patch`
## Unit test
`defects/kubernetes/unit/KubernetesTest.java`

View file

@ -0,0 +1,52 @@
# kubernetes-0002: Garbage collector — O(n) UID membership scan inside owner-reference loop
**Target:** kubernetes/kubernetes
**File:** `pkg/controller/garbagecollector/patch.go`
**Severity:** LOW-MEDIUM
**Pattern:** CWE-407 — linear membership test inside a loop
## Description
`deleteOwnerRefJSONMergePatch` iterates over all owner references on an object and for each one
calls `slices.Contains(ownerUIDs, ref.UID)` to decide whether to keep the reference.
`ownerUIDs` is a variadic `[]types.UID` slice; `slices.Contains` scans it linearly every
iteration.
Complexity: O(refs × ownerUIDs). In large clusters where objects have many owner references —
e.g. in garbage-collection cycles triggered by multi-owner CRDs or aggregated resources — this is
O(n²) over (refs, uids).
## Slow path
```go
refs := accessor.GetOwnerReferences()
for _, ref := range refs {
if !slices.Contains(ownerUIDs, ref.UID) { // O(ownerUIDs) each iteration
expectedObjectMeta.OwnerReferences = append(...)
}
}
```
## Fix
Build a `map[types.UID]struct{}` from `ownerUIDs` before the loop — O(U) build, O(1) per lookup:
```go
uidSet := make(map[types.UID]struct{}, len(ownerUIDs))
for _, uid := range ownerUIDs {
uidSet[uid] = struct{}{}
}
for _, ref := range refs {
if _, drop := uidSet[ref.UID]; !drop {
expectedObjectMeta.OwnerReferences = append(...)
}
}
```
## Patch
`defects/kubernetes/patch/kubernetes-0002.patch`
## Unit test
`defects/kubernetes/unit/KubernetesTest.java`

View file

@ -0,0 +1,87 @@
# linkerd2-0001: federatedService.update() slices.Contains O(n) inside loop → O(n²) dedup
**Target:** linkerd/linkerd2
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `controller/api/destination/federated_service_watcher.go`
**Status:** PATCHED
## Description
`federatedService.update()` computes the diff between the old and new
`remoteDiscovery` slices by calling `slices.Contains` inside two separate
`for range` loops. Each `slices.Contains` call is O(N) where N = number of
remote discovery IDs. With N IDs in both old and new slices, the diff is
O(N²).
This function is called on every Service update event. In multi-cluster Linkerd
deployments with many federated services, each annotation update triggers a
full O(N²) scan.
| Location | Pattern |
|----------|---------|
| `federated_service_watcher.go:231` | `slices.Contains(fs.remoteDiscovery, id)` inside `for _, id := range newRemoteDiscovery` |
| `federated_service_watcher.go:238` | `slices.Contains(newRemoteDiscovery, id)` inside `for _, id := range fs.remoteDiscovery` |
| `federated_service_watcher.go:46` | `remoteDiscovery []remoteDiscoveryID` — stored as slice |
## Root cause
```go
// federated_service_watcher.go:229
func (fs *federatedService) update(service *corev1.Service) {
newRemoteDiscovery := remoteDiscoveryIDs(service, fs.log)
// O(N²): for each new ID, scan old slice
for _, id := range newRemoteDiscovery {
if !slices.Contains(fs.remoteDiscovery, id) {
...subscribe...
}
}
// O(N²): for each old ID, scan new slice
for _, id := range fs.remoteDiscovery {
if !slices.Contains(newRemoteDiscovery, id) {
...unsubscribe...
}
}
fs.remoteDiscovery = newRemoteDiscovery
}
```
With N=1000 remote discovery IDs, each update event costs ~1,000,000 struct
comparisons. In active multi-cluster deployments this runs on every Service
reconcile.
## Fix
Convert `remoteDiscovery` to a `map[remoteDiscoveryID]struct{}` or use
`sets.Set[remoteDiscoveryID]`. Diff becomes O(N).
```go
// After fix: O(N) diff using map set
func (fs *federatedService) update(service *corev1.Service) {
newSet := make(map[remoteDiscoveryID]struct{})
for _, id := range remoteDiscoveryIDs(service, fs.log) {
newSet[id] = struct{}{}
}
oldSet := fs.remoteDiscoverySet
// O(N) adds
for id := range newSet {
if _, exists := oldSet[id]; !exists {
for i := range fs.subscribers { fs.remoteDiscoverySubscribe(...) }
}
}
// O(N) removes
for id := range oldSet {
if _, exists := newSet[id]; !exists {
for i := range fs.subscribers { fs.remoteDiscoveryUnsubscribe(...) }
}
}
fs.remoteDiscoverySet = newSet
}
```
## Ops/ns numbers (Java benchmark)
See `defects/linkerd2/unit/Linkerd2Test.java`.
At N=1000 IDs: slow ~1,000,000 ops, fast ~2,000 ops → >100× speedup.

View file

@ -0,0 +1,93 @@
# linux-0001: audit_filter_inodes — O(F²R) names_list re-scan per syscall
**File:** `kernel/auditsc.c`
**Severity:** HIGH — triggered on every syscall that touches audited inodes
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Call Chain
```
audit_filter_inodes() // called at syscall exit
list_for_each_entry(n, ctx->names_list) // O(F) — files touched by syscall
audit_filter_inode_name(tsk, n, ctx)
__audit_filter_op(tsk, ctx, inode_hash[h], name=n, op)
list_for_each_entry_rcu(e, inode_hash[h]) // O(R/B) — rules in bucket
audit_filter_rules(tsk, e->rule, ctx, name=n, ...)
for (i < field_count) // O(fields per rule)
case AUDIT_INODE:
case AUDIT_DEVMAJOR:
case AUDIT_DEVMINOR:
case AUDIT_OBJ_UID:
case AUDIT_OBJ_GID:
case AUDIT_OBJ_USER:
if (!name) {
list_for_each_entry(n, ctx->names_list) // O(F) again
}
```
`audit_filter_rules` is called with the current `name` pointer (non-NULL), so the
inner `ctx->names_list` re-scan at lines 572–624 is guarded by `else if (ctx)`.
However `audit_filter_syscall` invokes `__audit_filter_op(..., name=NULL, ...)` over the
`AUDIT_FILTER_EXIT` list — a flat list, not the inode hash. For every rule in that list
whose fields include `AUDIT_INODE`, `AUDIT_DEVMAJOR`, or the object ownership types, the
inner `list_for_each_entry(n, ctx->names_list)` fires unconditionally:
```
audit_filter_syscall()
__audit_filter_op(tsk, ctx, &audit_filter_list[AUDIT_FILTER_EXIT], name=NULL, op)
list_for_each_entry_rcu(e, list) // O(R) rules
audit_filter_rules(..., name=NULL) // inner names_list scan O(F) per inode-field
```
Total per-syscall cost: **O(R × fields × F)** where R = audit rules on EXIT list,
F = files touched per syscall. For a process that opens many files under a
directory audit watch (e.g., recursive compiler invocation), both R and F grow
independently and the product becomes dominant.
## Reproducing the Quadratic Growth
A process issuing N `open()` calls under an `auditctl -w /path -p rwxa` watch triggers
O(N × R) comparisons. With 100 audit rules and a syscall touching 100 files,
that is 10,000 comparisons instead of 200.
## Root Cause
`audit_filter_rules()` cannot match a specific field value against "any file in ctx"
without re-walking `ctx->names_list`. Because it is called per-rule rather than
per-name, and both R and F are unbounded, the combination is O(R × F).
## Fix
Two complementary approaches:
**A. Pass name=current_n from audit_filter_inodes path** (already done — correct).
The audit_filter_syscall path should similarly avoid the O(F) inner scan by
only checking rules that carry per-name field types via the inode hash, not the
flat EXIT list.
**B. For the EXIT list path** (audit_filter_syscall): pre-compute a per-context
bitset of observed dev/ino values into the audit_context at name-collection time.
Rules with AUDIT_INODE/AUDIT_DEVMAJOR etc. can then be checked in O(1) via
bitset membership rather than rescanning names_list.
```c
/* In struct audit_context, add: */
struct {
unsigned long ino_mask[BITS_TO_LONGS(AUDIT_INODE_BUCKETS)];
u32 devmajor_set[4]; /* sparse bitset for seen majors */
} fast_filter;
```
Set bits at `__audit_inode()` time; check bits in `audit_filter_rules()` before
the `list_for_each_entry` fallback.
## Impact
- Every `open(2)` / `openat(2)` under a directory watch runs O(R×F) comparisons.
- Compiler invocations (gcc, clang) touch hundreds of headers → audit load spikes.
- Scales linearly with both rule count and file count — O(n²) in the worst case.
## Patch
See `defects/linux/patch/linux-0002-audit-filter-inodes-quadratic.patch`

View file

@ -0,0 +1,94 @@
# linux-0002: __dev_alloc_name — O(D×A) nested sscanf on every interface rename
**File:** `net/core/dev.c`
**Function:** `__dev_alloc_name()` (line ~1358)
**Severity:** MEDIUM — triggered on every `ip link add`, `ip link set name`, container veth creation
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Code
```c
static int __dev_alloc_name(struct net *net, const char *name, char *res)
{
/* ... */
for_each_netdev(net, d) { // O(D) — all devices
struct netdev_name_node *name_node;
netdev_for_each_altname(d, name_node) { // O(A) — alt names per device
if (!sscanf(name_node->name, name, &i)) // string parse each time
continue;
if (i < 0 || i >= max_netdevices)
continue;
snprintf(buf, IFNAMSIZ, name, i);
if (!strncmp(buf, name_node->name, IFNAMSIZ))
__set_bit(i, inuse);
}
/* same sscanf/snprintf/strncmp on d->name */
}
i = find_first_zero_bit(inuse, max_netdevices);
/* ... */
}
```
## Complexity
| Variable | Meaning |
|----------|---------|
| D | Number of net devices in the namespace |
| A | Number of alternative names per device |
Total work per call: **O(D × A × sscanf_cost)**.
`sscanf` with a format containing `%d` is not O(1); it involves format string parsing.
Container-heavy hosts (Kubernetes nodes) routinely carry D=500+ veth/bridge/vlan
devices, each with 1-3 alt names from `ip link property add`.
## When Triggered
- `ip link add vethN type veth` in a pod namespace — creates two interfaces,
both call `dev_alloc_name()` with format `"veth%d"`.
- A node creating 100 pods triggers 200 calls; if D=400 devices already exist
with A=2 alt names, each call walks 800 entries.
- Batch pod launches show O(D²) total work as D grows.
## Root Cause
The bitmap approach is correct for the final `find_first_zero_bit`, but the bitmap
is populated by rescanning all devices + alt names on every call. The primary
device name already uses the hash (`dev_name_hash`), but alt names bypass the hash
and fall into the linear `netdev_for_each_altname` walk.
## Fix
Maintain a sorted or hash-keyed index of all allocated numeric suffixes per name
prefix. On device/altname registration, insert the suffix into the prefix's free
map; on deregistration, remove it. `__dev_alloc_name` then does one map lookup
to find the first free slot in O(log D) or O(1) amortized.
Simpler interim fix: skip the `netdev_for_each_altname` inner loop when counting
in-use slots for the primary name format, since alt names use different formats
(verified by `netdev_name_node_alt_create` — alt names are explicit strings, not
`%d` patterns). Add a guard:
```c
netdev_for_each_altname(d, name_node) {
/* Alt names created via 'ip link property add' are never %d patterns;
* skip sscanf unless the alt name could plausibly match. */
if (!memchr(name_node->name, '0' + (i % 10), IFNAMSIZ))
continue; /* fast reject — not numeric */
/* ... existing sscanf logic ... */
}
```
Full fix: maintain per-prefix bitmaps in a global xarray keyed by name prefix hash.
## Impact
- O(D×A) work per interface creation; D and A both grow with container density.
- On a Kubernetes node with 500 pods: ~1000 devices × 2 alt names = 2000 sscanf
calls per new pod creation, versus O(1) with an index.
- Multiplied by pod churn rate (100/min), this is 200,000 sscanf calls/min.
## Patch
See `defects/linux/patch/linux-0003-dev-alloc-name-nested-altname.patch`

View file

@ -0,0 +1,102 @@
# linux-0003: lookup_neigh_parms — O(P) linear scan by ifindex on every netlink neigh table op
**File:** `net/core/neighbour.c`
**Function:** `lookup_neigh_parms()` (line ~1752)
**Severity:** MEDIUM — triggered on every `ip neigh` operation and ARP/NDP table config
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Code
```c
static inline struct neigh_parms *lookup_neigh_parms(struct neigh_table *tbl,
struct net *net,
int ifindex)
{
struct neigh_parms *p;
list_for_each_entry(p, &tbl->parms_list, list) { // O(P) linear scan
if ((p->dev && p->dev->ifindex == ifindex &&
net_eq(neigh_parms_net(p), net)) ||
(!p->dev && !ifindex && net_eq(net, &init_net)))
return p;
}
return NULL;
}
```
`tbl->parms_list` holds one `neigh_parms` per network device that has joined the
neighbour table. In environments with many network devices (bridges, VLANs, VxLAN
tunnels, bond members), this list grows to O(D) entries.
## Complexity
| Variable | Meaning |
|----------|---------|
| P | Length of tbl->parms_list — one entry per netdev registered with this neigh_table |
`lookup_neigh_parms` is called from `neigh_table_set_key()` (netlink path) whenever
`ip neigh change`, `ip neigh add`, or `ip ntable change` is issued. On a host
with D=300 network interfaces (common in VxLAN fabrics), each such command walks
300 parms entries.
## When Triggered
```
ip ntable change name arp dev eth0 # calls lookup_neigh_parms O(P)
ip neigh change 192.168.1.1 dev eth0 ... # calls neigh_lookup + parms lookup
```
Automation scripts that reconfigure neighbour parameters across many interfaces
(e.g., setting `base_reachable_time` for all VTEP devices) issue O(D) netlink
commands, each doing an O(D) scan → O(D²) total.
## Root Cause
`parms_list` is a flat linked list ordered by insertion. Lookup by `ifindex` is
O(P) because there is no secondary index.
The natural key for `neigh_parms` is `(net, ifindex)`. An xarray keyed by ifindex
within each `net` provides O(1) lookup with no extra memory per entry.
## Fix
Replace the `parms_list` linear search with an xarray stored in `struct neigh_table`:
```c
/* In struct neigh_table (include/net/neighbour.h): */
struct xarray parms_xa; /* keyed by ifindex, value = neigh_parms * */
struct list_head parms_list; /* keep for iteration (GC, sysctl dumps) */
```
```c
/* neigh_parms_alloc: */
xa_store(&tbl->parms_xa, p->dev ? p->dev->ifindex : 0, p, GFP_KERNEL);
/* lookup_neigh_parms replacement: */
static inline struct neigh_parms *lookup_neigh_parms(struct neigh_table *tbl,
struct net *net,
int ifindex)
{
struct neigh_parms *p = xa_load(&tbl->parms_xa, ifindex);
if (p && net_eq(neigh_parms_net(p), net))
return p;
return NULL;
}
/* neigh_parms_release: */
xa_erase(&tbl->parms_xa, p->dev ? p->dev->ifindex : 0);
```
The `parms_list` is retained for the GC timer path (`neigh_periodic_work`) which
iterates all parms to call `neigh_set_reach_time`.
## Impact
- O(D) per netlink command; O(D²) for configuration scripts covering all devices.
- On a VxLAN gateway with 500 VTEPs: 500 parms entries × 500 commands = 250,000
list-node comparisons per configuration pass.
- With xarray: 500 commands × O(1) = 500 xa_load calls.
## Patch
See `defects/linux/patch/linux-0004-neigh-parms-xarray-lookup.patch`

View file

@ -0,0 +1,59 @@
# love2d-0001 — getJoystickFromID O(n) linear scan per SDL joystick event
## Status
PATCHED
## Severity
MEDIUM
## Target
love2d `src/modules/joystick/sdl/JoystickModule.cpp`
## CWE
CWE-407: Algorithmic Complexity — Insufficient Complexity Reduction Before Algorithmic
Intensive Operation
## Description
`JoystickModule::getJoystickFromID(int instanceid)` performs an O(n) linear scan of
`activeSticks` (a `std::vector<Joystick*>`) to find a joystick by its SDL instance ID.
This function is called once per SDL joystick event in the SDL event dispatch loop.
```cpp
// JoystickModule.cpp:98
love::joystick::Joystick *JoystickModule::getJoystickFromID(int instanceid)
{
for (auto stick : activeSticks) // O(n) every event
{
if (stick->getInstanceID() == instanceid)
return stick;
}
return nullptr;
}
```
## Call Sites (Event.cpp)
```
Event.cpp:640 getJoystickFromID(e.jbutton.which)
Event.cpp:652 getJoystickFromID(e.jaxis.which)
Event.cpp:667 getJoystickFromID(e.jhat.which)
Event.cpp:686 getJoystickFromID(b.which)
Event.cpp:703 getJoystickFromID(a.which)
Event.cpp:725 getJoystickFromID(e.jdevice.which)
Event.cpp:737 getJoystickFromID(sens.which)
```
7 separate event types, each calling O(n) scan. With N connected joysticks and E events
per frame, cost is O(N * E) per frame update.
## Fix
Replace the `std::vector<Joystick*> activeSticks` scan with a
`std::unordered_map<int, Joystick*> sticksById` keyed on instance ID.
Updated on `addJoystick`/`removeJoystick`. Lookup becomes O(1).
## Complexity
- Before: O(n) per event where n = number of active joysticks
- After: O(1) per event
## Files
- `defects/love2d/patch/love2d-0001.patch`
- `defects/love2d/unit/Love2dJoystickLookupTest.java`

View file

@ -0,0 +1,74 @@
# lua-0001: searchupvalue() O(n) linear scan per variable reference during compilation
**Target:** lua/lua
**Severity:** LOW-MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `lparser.c` — `searchupvalue()` (~line 360), called from `singlevaraux()`
**Status:** PATCHED
## Description
During Lua compilation, every reference to a variable name goes through
`singlevaraux()` → `searchupvalue()`. `searchupvalue()` performs a linear scan
of the `FuncState.f->upvalues` array (up to `MAXUPVAL` = 255 entries) to find
an existing upvalue with the given name. A function with many upvalues that
references them frequently (e.g. in a tight inner loop body) pays O(n) per
reference at compile time. With M references and N upvalues: O(M × N).
Lua limits upvalues to 255 per function (`MAXUPVAL`), so the worst-case scan
length is bounded, but the constant is large: 255 × (number of variable
references in body). A deeply nested function closure with 200 upvalues and a
loop body with 500 variable references performs ~100,000 string pointer
comparisons at compile time.
## Root cause
```c
/* lparser.c ~360 */
static int searchupvalue (FuncState *fs, TString *name) {
int i;
Upvaldesc *up = fs->f->upvalues;
for (i = 0; i < fs->nups; i++) { /* O(n) linear scan */
if (eqstr(up[i].name, name)) return i;
}
return -1;
}
```
`eqstr` is a pointer comparison (Lua interns all strings), so each iteration is
cheap, but the scan still walks the entire upvalue list.
## Fix
Add a small hash map (`TString* → upvalue index`) to `FuncState`, populated in
`newupvalue()`. `searchupvalue()` becomes a single hash lookup.
Since `FuncState` is stack-allocated during parsing, a fixed-size open-address
table (power-of-two slots, 256 entries) fits without heap allocation:
```c
/* In FuncState (lparser.h): */
int upval_map[256]; /* slot → upvalue index; -1 = empty */
/* newupvalue(): after allocating */
int slot = luaO_str2num(name) & 255; /* or name->hash & 255 */
/* linear probe on collision */
upval_map[slot] = fs->nups - 1;
/* searchupvalue(): */
static int searchupvalue (FuncState *fs, TString *name) {
int slot = name->hash & 255;
/* probe up to 8 slots */
for (int probe = 0; probe < 8; probe++, slot = (slot+1)&255) {
int idx = fs->upval_map[slot];
if (idx < 0) return -1;
if (eqstr(fs->f->upvalues[idx].name, name)) return idx;
}
return searchupvalue_fallback(fs, name); /* full scan on overflow */
}
```
## Ops numbers (Java benchmark)
See `defects/lua/unit/LuaTest.java` (bench label "searchupvalue").
At N=200 upvalues, M=500 references: slow ~100,000 comparisons, fast ~500 → 200× speedup.

View file

@ -0,0 +1,54 @@
# mariadb-0001 — setup_order/setup_group: O(ORDER × SELECT) find_item_in_list quadratic
**Target:** MariaDB/server
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Files:**
- `sql/sql_select.cc` lines 28873–28876 (`setup_order`)
- `sql/sql_select.cc` lines 28950–28953 (`setup_group`)
**Status:** PATCHED
---
## Defect
Both `setup_order()` and `setup_group()` iterate over the ORDER BY / GROUP BY
item linked list and for each item call `find_order_in_list()` (lines 28875,
28952), which itself calls `find_item_in_list()` — a linear O(S) walk over
`List<Item> &fields` (the SELECT list) implemented at `sql/sql_base.cc:7175`.
This gives **O(O × S)** complexity where O = number of ORDER/GROUP BY columns
and S = number of SELECT columns. For a query with 100 ORDER BY expressions
and a 100-column SELECT list this is 10 000 string comparisons per query
parse/resolution pass.
`find_item_in_list` uses `List_iterator<Item>` (a singly-linked list iterator)
so there is no random access and no shortcut — every call scans from the
head.
The same `find_item_in_list` is also called from `setup_new_fields()`
(line 29062) in a loop over a new-fields linked list, compounding the issue
for INSERT ... SELECT or multi-table updates with many expressions.
## Root Cause
`List<Item>` (MariaDB's intrusive linked list) has no index. Resolving ORDER
BY / GROUP BY positions against the SELECT list requires a name lookup, and
the current code rebuilds this O(S) walk for every ORDER/GROUP item without
caching a name→position map.
## Fix
Before the `for` loop in `setup_order` and `setup_group`, build a
`std::unordered_map<std::string, uint>` mapping field name to SELECT-list
position. Each `find_order_in_list` call can then resolve positional and
name-based references in O(1) instead of O(S).
See: `defects/mariadb/patch/mariadb-0001.patch`
## Benchmark
`defects/mariadb/unit/MariadbTest.java` — `setup_order quadratic` test case:
- Slow (List_iterator linear scan per ORDER item): O(n²)
- Fast (HashMap pre-indexed): O(n), speedup ≥ 15× at n=500

View file

@ -0,0 +1,38 @@
# mariadb-0002 — setup_new_fields: O(N × S) find_item_in_list inside new-fields loop
**Target:** MariaDB/server
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `sql/sql_select.cc` lines 29060–29064
**Status:** PATCHED
---
## Defect
`setup_new_fields()` iterates a linked list of `new_field` ORDER objects
(line 29060) and for each calls `find_item_in_list(*new_field->item, fields, ...)`
(line 29062) — an O(S) linear scan over the SELECT list.
This gives **O(N × S)** complexity where N = length of the new-fields list
and S = SELECT-list size. In multi-table UPDATE or INSERT ... SELECT with
many computed columns, both N and S can reach the hundreds.
## Root Cause
Same root cause as mariadb-0001: `List<Item>` has no hash index and
`find_item_in_list` always walks from the head.
## Fix
Build the same `unordered_map<string, uint>` name→position index that
mariadb-0001's fix introduces for `setup_order`/`setup_group`, and reuse it
in `setup_new_fields`. If the callers are separate, build the map locally
at the top of `setup_new_fields`.
See: `defects/mariadb/patch/mariadb-0002.patch`
## Benchmark
Covered by `defects/mariadb/unit/MariadbTest.java` — `setup_new_fields quadratic`
test case.

View file

@ -0,0 +1,38 @@
# maven-0001 — DefaultLifecycleExecutionPlanCalculator: List.of().contains() rebuilt per mojo execution
## Target
`apache/maven` — `impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleExecutionPlanCalculator.java`
## CWE
CWE-407: Algorithmic Complexity — O(n) linear membership inside a hot loop
## Location
```
DefaultLifecycleExecutionPlanCalculator.java:266
if (List.of(DefaultLifecycles.STANDARD_LIFECYCLES).contains(lifecycle.getId())) {
```
## Description
`calculateLifecycleMappings()` is called once per lifecycle phase per mojo execution during plan
calculation. On every invocation it calls `List.of(DefaultLifecycles.STANDARD_LIFECYCLES)` —
allocating a new 3-element `List` — then calls `.contains()` on it, scanning linearly.
`STANDARD_LIFECYCLES` is the static array `{"clean", "default", "site"}`.
The defect is twofold:
1. A new `List` object is heap-allocated on every call.
2. Linear scan over the list instead of O(1) set membership.
For a multi-module Maven build with M modules and N mojos per lifecycle, this executes M × N times.
## Complexity
- Slow path: O(|STANDARD_LIFECYCLES|) per call = O(3) per call, but M × N calls
- Fast path: O(1) with a precomputed `Set` constant
## Patch
`defects/maven/patch/maven-0001-standard-lifecycle-set.patch`
## Unit Test
`defects/maven/unit/MavenLifecycleStandardSetTest.java`
## Status
PATCHED

View file

@ -0,0 +1,71 @@
# memcached-0001: slabs_clsid() O(n) linear scan over sorted array
**Target:** memcached/memcached
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `slabs.c:77` (`slabs_clsid`)
**Status:** PATCHED
## Description
`slabs_clsid(size_t size)` finds the smallest slab class whose chunk size
accommodates the requested allocation. The slab class array (`slabclass[]`) is
sorted in ascending order of `size`. The current implementation walks it
linearly with a `while` loop.
```c
unsigned int slabs_clsid(const size_t size) {
int res = POWER_SMALLEST;
if (size == 0 || size > settings.item_size_max)
return 0;
while (size > slabclass[res].size)
if (res++ == power_largest)
return power_largest;
return res;
}
```
`power_largest` reaches up to 63 (MAX_NUMBER_OF_SLAB_CLASSES − 1 = 63).
Binary search over a sorted array of 63 entries requires at most 6 comparisons
(⌈log₂(63)⌉ = 6) vs. up to 63 comparisons in the linear case — a ~10× speedup.
`slabs_clsid` is called on **every item allocation**:
- `do_item_alloc` — called for every `SET` / `ADD` / `REPLACE` / `APPEND` /
`PREPEND` / `CAS` command
- `do_item_alloc_chunk` — called per chunk of large items
- `item_store_check` — called during `SET` pre-validation
Under high write throughput this is a hot path: a 200k SET/s workload calls
`slabs_clsid` ~200,000 times per second.
## Fix
Replace the `while` loop with a binary search over `slabclass[1..power_largest]`:
```c
unsigned int slabs_clsid(const size_t size) {
if (size == 0 || size > settings.item_size_max)
return 0;
int lo = POWER_SMALLEST, hi = power_largest;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (slabclass[mid].size < size)
lo = mid + 1;
else
hi = mid;
}
return lo;
}
```
This is safe because `slabclass[].size` is strictly monotonically increasing
(guaranteed by the `slabs_init` construction loop).
See patch: `defects/memcached/patch/0001-slabs-clsid-binary-search.patch`
## Ops ratio (unit test)
63 slab classes, query for the largest size:
- slow ops: 63 (linear scan)
- fast ops: 6 (binary search, ⌈log₂(63)⌉)
- speedup: **10.5×**

View file

@ -0,0 +1,48 @@
# mongodb-0001: RelevantTag first/notFirst vector linear membership scan
**Target:** mongodb/mongo
**File:** `src/mongo/db/query/index_tag.h`,
`src/mongo/db/query/planner_ixselect.cpp`
**Severity:** MEDIUM
**Pattern:** CWE-407 — O(n) membership test inside loop → O(n²)
## Description
`RelevantTag` (used during index selection in the query planner) stores the set
of index IDs that can satisfy a predicate in two `std::vector<size_t>` fields:
`first` (leading-field indices) and `notFirst` (non-leading-field indices).
Three functions in `planner_ixselect.cpp` perform O(n) linear `std::find` scans
on these vectors inside loops over AST children:
- `isIndexAssigned()` — called while stripping invalid compound-wildcard index
assignments (loop over children × `std::find` on `first`/`notFirst`)
- `stripInvalidAssignmentsToTextIndex()` — loop over AND children × `std::find`
on `first`/`notFirst`
- `removeIndexRelevantTag()` — called from several strip-invalid loops ×
`std::find` + `erase` on `first`/`notFirst`
A query with P predicates each tagged with I candidate indices executes
O(P × I) linear probes → **O(P·I²)** total work for the strip passes.
## Reproduction
A compound text or 2dsphere query with many predicates and many applicable
indices triggers repeated `stripInvalidAssignments*` calls. Each call does
O(predicates × indices) linear scans over the tag vectors.
## Fix
Change `RelevantTag::first` and `RelevantTag::notFirst` from
`std::vector<size_t>` to `std::unordered_set<size_t>`. Membership tests
become O(1); iteration and copy semantics remain the same.
See `defects/mongodb/patch/0001.patch`.
## Complexity
| Operation | Before | After |
|-----------|--------|-------|
| membership test (`isIndexAssigned`) | O(n) | O(1) |
| `removeIndexRelevantTag` | O(n) search + O(n) erase | O(1) each |
| strip pass total | O(P·I) | O(P) |

View file

@ -0,0 +1,48 @@
# mysql-0001 — SHOW GRANTS USING: O(R²) role membership scan
**Target:** mysql/mysql-server
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `sql/auth/sql_authorization.cc`
**Lines:** 4875–4898
**Status:** PATCHED
---
## Defect
`mysql_show_grants()` validates a `SHOW GRANTS ... USING <role_list>` statement
by iterating every role in `using_roles` (outer loop, line 4880) and for each
calling `std::find` on `granted_roles` — a `std::vector<std::pair<Role_id,bool>>`
(line 4882) — and then `std::find_if` on `mandatory_roles` — also a `std::vector`
(line 4884).
Both inner searches are O(G) and O(M) respectively, giving a total complexity of
**O(U × (G + M))** where U = `|using_roles|`, G = `|granted_roles|`, M =
`|mandatory_roles|`.
When a user has many granted roles (e.g. in an RBAC-heavy application with
hundreds of roles, or the mandatory-roles list is long), every
`SHOW GRANTS ... USING` call burns O(n²) CPU inside the privilege validator.
This path is also reached on every `SET ROLE` + implicit SHOW which means
application-tier connection pooling can trigger it on every checkout.
## Root Cause
`List_of_granted_roles` is `std::vector<std::pair<Role_id, bool>>` (defined in
`sql/auth/auth_internal.h:268`). No hash index is built before the loop.
## Fix
Build an `unordered_set` of the `authid` strings from `granted_roles` and a
parallel `unordered_set` from `mandatory_roles` before the outer loop.
Membership is then O(1) per lookup.
See: `defects/mysql/patch/mysql-0001.patch`
## Benchmark
`defects/mysql/unit/MysqlTest.java` — `SHOW GRANTS USING quadratic` test case:
- Slow (vector find): ~O(n²), confirmed by ops count
- Fast (unordered_set): ~O(n), speedup ≥ 10× at n=500

View file

@ -0,0 +1,57 @@
# mysql-0002 — has_global_grant: O(P) linear privilege scan in ACL-map fallback
**Target:** mysql/mysql-server
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `sql/auth/sql_security_ctx.cc`
**Line:** 737
**Status:** PATCHED
---
## Defect
`Security_context::has_global_grant(const char *, size_t)` contains two paths
for checking dynamic privileges:
1. **Fast path** (line 743–747): when `m_acl_map != nullptr`, uses
`unordered_map::find` — O(1).
2. **Slow path** (line 735–740): when `m_acl_map == nullptr` (e.g. during
bootstrap, after ACL reload, or in edge-case threaded scenarios), calls
`get_dynamic_privileges_map()->equal_range(key)` then
`std::find(it, it_end, privilege)` — **O(P)** linear scan over all
privileges for this user in the multimap's equal range.
`User_to_dynamic_privileges_map` is `std::unordered_multimap<Role_id,
Grant_privilege>` (defined in `auth_internal.h:316`). The `equal_range`
returns all entries for the user (up to P items), and `std::find` walks
them linearly comparing `std::pair<string,bool>` against the target privilege
string.
The slow path is hit whenever the security context has not yet loaded its
ACL map — this occurs on the first check per connection after a FLUSH
PRIVILEGES or during multi-threaded plugin init. In a system with 200 dynamic
privileges per user, every check in the slow path costs O(200) string
comparisons.
## Root Cause
The multimap fallback lacks a local hash index. After `equal_range` the code
should use a temporary `unordered_set` or simply switch the inner storage for
this user's privileges to a sorted vector so binary_search can be used.
## Fix
Replace the `std::find` linear scan with a local `unordered_set<std::string>`
built from the equal_range result, then do O(1) lookup. Or prefer
`std::find_if` with early exit and add a comment explaining that callers
should populate `m_acl_map` to avoid this path.
See: `defects/mysql/patch/mysql-0002.patch`
## Benchmark
`defects/mysql/unit/MysqlTest.java` — `has_global_grant fallback` test case:
- Slow (linear equal_range scan): O(n) per check
- Fast (unordered_set): O(1) per check, speedup ≥ 20× at n=500

View file

@ -0,0 +1,55 @@
# nats-0001 — jetstream_cluster: O(n²) slices.Contains in peer-set dedup loop
**Target:** nats-io/nats-server
**File:** `server/jetstream_cluster.go`
**Function:** stream update/move handler (jsStreamUpdateRequest path)
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic Membership Test)
**Status:** PATCHED
## Defect
During a JetStream stream move operation, existing peers are deduplicated against a new
peer group using a nested scan:
```go
// filter peers present in both sets
for _, peer := range rg.Peers {
if !slices.Contains(nrg.Peers, peer) {
peerSet = append(peerSet, peer)
}
}
peerSet = append(peerSet, nrg.Peers...)
```
`rg.Peers` and `nrg.Peers` are `[]string` slices. `slices.Contains` is O(|nrg.Peers|).
The outer loop runs O(|rg.Peers|) times. Total: **O(|rg.Peers| × |nrg.Peers|)**.
In a large JetStream cluster (hundreds of streams, many replicas), this is called once per
stream move/scale operation and degrades quadratically with replica count and peer count.
## Fix
Build a set from `nrg.Peers` before the loop:
```go
// filter peers present in both sets
nrgPeerSet := make(map[string]struct{}, len(nrg.Peers))
for _, p := range nrg.Peers {
nrgPeerSet[p] = struct{}{}
}
for _, peer := range rg.Peers {
if _, ok := nrgPeerSet[peer]; !ok {
peerSet = append(peerSet, peer)
}
}
peerSet = append(peerSet, nrg.Peers...)
```
## Patch
`defects/nats-server/patch/nats-0001-peer-dedup-map.patch`
## Unit Test
`defects/nats-server/unit/NatsPeerDedupTest.java`

View file

@ -0,0 +1,51 @@
# nginx-0001: ngx_http_upstream_cache_get linear cache-zone name scan
**Target:** nginx
**File:** `src/http/ngx_http_upstream.c`
**Function:** `ngx_http_upstream_cache_get`
**Lines:** 1058–1069
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
When a proxy_cache directive uses a variable (`$variable`) rather than a literal
zone name, nginx resolves the zone on every request by walking the `u->caches`
array with `ngx_strncmp` comparisons. With N configured cache zones and R
requests per second the total work is O(N × R).
```c
caches = u->caches->elts;
for (i = 0; i < u->caches->nelts; i++) {
name = &caches[i]->shm_zone->shm.name;
if (name->len == val.len
&& ngx_strncmp(name->data, val.data, val.len) == 0)
{
*cache = caches[i];
return NGX_OK;
}
}
```
## Impact
- Real-world configs with multiple cache zones (e.g. tiered caching, per-vhost
zones) make this a hot O(N) scan on every cacheable proxy request.
- N is bounded by the number of `proxy_cache_zone` directives (typically 2–8),
but the scan repeats for every request where `proxy_cache` is a variable.
## Fix
Build a hash map from zone name → `ngx_http_file_cache_t *` at configuration
time. At request time use `ngx_hash_find` (O(1)) instead of the linear scan.
The map key is the shm zone name (ASCII, lowercase-normalizable).
## Patch
See `defects/nginx/patch/nginx-0001.patch`
## Unit Test
See `defects/nginx/unit/NginxCacheGetAlgorithmTest.java`

View file

@ -0,0 +1,49 @@
# odl-0002: ShardManager ShardManagerSnapshot.getShardList().contains() — O(n) ImmutableList membership
## Status
PATCHED
## Target
opendaylight/controller
## File
`opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/ShardManagerSnapshot.java`
`opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/shardmanager/ShardManager.java`
## Symptom
On every `CreateShard` event, `ShardManager.doCreateShard()` calls
`currentSnapshot.getShardList().contains(shardName)` to check whether the shard was
present in the recovered snapshot. `getShardList()` returns an `ImmutableList<String>`.
With S shards in the snapshot, this is O(S) per shard creation. During cluster recovery
with hundreds of shards, every shard recreation pays O(S) to check membership in the
snapshot — O(S²) total across all creations.
## Root cause
```java
// ShardManagerSnapshot.java:31
public List<String> getShardList() {
return shardList; // ImmutableList — O(n) contains
}
// ShardManager.java:543-544
boolean shardWasInRecoveredSnapshot = currentSnapshot != null
&& currentSnapshot.getShardList().contains(shardName);
```
`ImmutableList.contains()` is O(n) array scan.
## Fix
Change `shardList` storage in `ShardManagerSnapshot` to `ImmutableSet<String>`. Rename
`getShardList()` → `getShardNames()` returning `Set<String>` (or keep both for
compatibility). `ImmutableSet.contains()` is O(1).
## Complexity
| Before | After |
|--------|-------|
| O(S) per CreateShard | O(1) per CreateShard |
## Patch
`defects/odl/patch/odl-0002-shardmanager-snapshot-shardlist-linear.patch`
## Unit test
`defects/odl/unit/OdlShardManagerSnapshotTest.java`

View file

@ -0,0 +1,54 @@
# onos-0002: PipelineTraceableHitChain addDataPlaneEntity — O(n²) ArrayList membership test
## Status
PATCHED
## Target
opennetworkinglab/onos
## File
`core/api/src/main/java/org/onosproject/net/PipelineTraceableHitChain.java`
## Symptom
`addDataPlaneEntity()` calls `hitChain.contains()` on an `ArrayList<DataPlaneEntity>` before
every insert — O(n) linear scan. The method is called inside O(n) loops in
`OfdpaPipelineTraceable` (lines 267 and 683), producing O(n²) total work during pipeline
trace chain copying. With deep group-bucket trees (typical in OpenFlow 1.3 table-miss
chains), hitChain can contain dozens to hundreds of entries. Each `forEach` → `addDataPlaneEntity`
copy costs O(n) per element → O(n²) total.
## Root cause
```java
// PipelineTraceableHitChain.java:94
public void addDataPlaneEntity(DataPlaneEntity dataPlaneEntity) {
if (!hitChain.contains(dataPlaneEntity)) { // O(n) ArrayList scan
hitChain.add(dataPlaneEntity);
}
}
```
`hitChain` is declared as `List<DataPlaneEntity>` backed by `ArrayList`. The dedup check
is O(n) per call. Callers iterate over the existing chain and call this method once per
element — making every chain copy O(n²).
**Call site example** (OfdpaPipelineTraceable.java:683):
```java
currentHitChain.hitChain().forEach(newHitChain::addDataPlaneEntity);
```
With a 50-entry hit chain this issues 50 × 50 = 2500 equality comparisons for a single
packet trace.
## Fix
Replace `ArrayList` with `LinkedHashSet` (insertion-ordered set). `contains()` becomes
O(1); iteration order is preserved; add/remove semantics are unchanged for the callers.
## Complexity
| Before | After |
|--------|-------|
| O(n²) chain copy | O(n) chain copy |
## Patch
`defects/onos/patch/onos-0002-pipeline-hitchain-arraylist-quadratic.patch`
## Unit test
`defects/onos/unit/OnosPipelineHitChainTest.java`

View file

@ -0,0 +1,44 @@
# onos-0003: DeviceManager RoleInfo.backups() ImmutableList.contains() — O(n) per role event
## Status
PATCHED
## Target
opennetworkinglab/onos
## File
`core/api/src/main/java/org/onosproject/cluster/RoleInfo.java`
`core/net/src/main/java/org/onosproject/net/device/impl/DeviceManager.java`
## Symptom
On every mastership role change event, `DeviceManager` checks whether `localNodeId` is in
the backup list using `List.contains()` on an `ImmutableList`. With C ONOS cluster nodes,
the backup list has up to C-1 entries. In large clusters (10–50 nodes) this is O(C) per
device event. With D managed devices, each failover triggers D events — O(D×C) total.
## Root cause
```java
// RoleInfo.java:32,36
private final List<NodeId> backups;
this.backups = ImmutableList.copyOf(backups); // O(n) contains
// DeviceManager.java:1159
} else if (event.roleInfo().backups().contains(localNodeId)) {
```
`backups()` returns `ImmutableList<NodeId>` — linear scan backed by an array.
## Fix
Change `backups` to `ImmutableSet<NodeId>`. Order of backup list is not meaningful for
membership tests; `ImmutableSet.contains()` is O(1) hash lookup.
## Complexity
| Before | After |
|--------|-------|
| O(C) per role event | O(1) per role event |
## Patch
`defects/onos/patch/onos-0003-roleinfo-backups-immutablelist-linear.patch`
## Unit test
`defects/onos/unit/OnosRoleInfoTest.java`

View file

@ -0,0 +1,46 @@
# openssl-0001: SSL_get_shared_ciphers — O(n²) cipher intersection via unsorted sk_SSL_CIPHER_find
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Severity:** MEDIUM
**Status:** PATCHED
**File:** `ssl/ssl_lib.c` — `SSL_get_shared_ciphers()`
## Defect
`SSL_get_shared_ciphers()` iterates all client cipher suites (outer loop, O(n)) and for
each one calls `sk_SSL_CIPHER_find(srvrsk, c)` on the server cipher stack.
`sk_SSL_CIPHER_find` → `OPENSSL_sk_find` → `internal_find`: when the stack has no
comparator set **or is not sorted**, falls through to a linear scan (`for i = 0; i < st->num`).
The compare function is intentionally NOT set on `srvrsk` here — the comment in `s3_lib.c`
(line 4808) says: "Do not set the compare functions, because this may lead to a reordering
by 'id'. We want to keep the original ordering. We may pay a price in performance during
sk_SSL_CIPHER_find()."
Result: **O(n × m)** where n = client cipher count, m = server cipher count.
TLS 1.2 supports ~100 cipher suites; hostile clients can send 100 → 10,000 comparisons
per `SSL_get_shared_ciphers()` call.
```c
// ssl/ssl_lib.c:3618
for (i = 0; i < sk_SSL_CIPHER_num(clntsk); i++) { // O(n)
c = sk_SSL_CIPHER_value(clntsk, i);
if (sk_SSL_CIPHER_find(srvrsk, c) < 0) // O(m) — linear when unsorted
continue;
...
}
```
## Fix
Build a `uint32_t` bitset or `LHASH`/hash-set of server cipher IDs before the loop.
Membership check becomes O(1); total complexity O(n + m).
See patch: `defects/openssl/patch/openssl-0001.patch`
## Hot Path
Called by diagnostic/logging code and TLS session inspection. Not the negotiation fast path
(`ssl_choose_cipher` uses a pre-sorted `cipher_list_by_id`) but still reachable per
connection on servers that log shared cipher info.

View file

@ -0,0 +1,32 @@
# openssl-0002: ciphersuite_cb — O(n²) TLS 1.3 cipher list dedup via linear scan
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Severity:** LOW (TLS 1.3 has only 5 standard suites; attack surface is config-time)
**Status:** PATCHED
**File:** `ssl/ssl_ciph.c` — `ciphersuite_cb()`
## Defect
`set_ciphersuites()` parses a colon-separated TLS 1.3 cipher string by calling
`CONF_parse_list(..., ciphersuite_cb, newciphers)`. `CONF_parse_list` invokes
`ciphersuite_cb` once per token (O(n) outer iterations). Inside the callback, a
duplicate-suppression loop scans all already-added ciphers linearly:
```c
// ssl/ssl_ciph.c:1250
for (int i = 0; i < sk_SSL_CIPHER_num(ciphersuites); ++i) // O(k) inner
if (sk_SSL_CIPHER_value(ciphersuites, i)->id == cipher->id)
return 1;
```
With n tokens this is O(n²) dedup. Today TLS 1.3 has 5 ciphersuites so practical
cost is negligible, but the pattern is wrong and will degrade if the list ever grows
(e.g. custom/vendor forks, future RFC additions).
## Fix
Replace the linear scan with a `uint8_t seen[SSL_MAX_CIPHERS]` bitmask indexed by the
cipher's position in the static `tls13_ciphers` table (all IDs are known at compile
time). Lookup and mark become O(1).
See patch: `defects/openssl/patch/openssl-0002.patch`

View file

@ -0,0 +1,48 @@
# openvpn-0001: ncp_get_best_cipher / tls_item_in_cipher_list — O(n²) cipher negotiation
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Severity:** MEDIUM
**Status:** PATCHED
**File:** `src/openvpn/ssl_ncp.c` — `ncp_get_best_cipher()`, `tls_item_in_cipher_list()`
## Defect
`tls_item_in_cipher_list(item, list)` is an O(m) linear scan: it calls `string_alloc`
(heap allocation), then `strtok` walks the colon-separated `list` comparing each token
with `strcmp`.
`ncp_get_best_cipher()` (and three other call sites) runs an **outer** `while (strsep)`
loop over the server cipher list and calls `tls_item_in_cipher_list` inside:
```c
// ssl/ssl_ncp.c:270
while ((token = strsep(&tmp_ciphers, ":"))) // O(n) outer
{
if (tls_item_in_cipher_list(token, peer_ncp_list) // O(m) inner + malloc+free
|| streq(token, remote_cipher))
break;
}
```
Total: **O(n × m)** comparisons plus O(n) heap allocations per call.
This runs at TLS handshake time for every new connection.
Additional O(n²) call sites (all share the same root cause):
- `ssl_ncp.c:388` — `p2p_ncp_get_common_cipher` inner loop
- `dco.c:468` — `dco_check_option_conflict` inner loop
## Fix
Pre-split `peer_ncp_list` once into a fixed-size set (at most ~20 cipher names).
Membership test becomes an O(m) one-time split + O(1) amortized per outer iteration
(hash set or sorted array + bsearch). Eliminates repeated `string_alloc`/`strtok`.
Minimal fix: split once before outer loop, compare against array.
See patch: `defects/openvpn/patch/openvpn-0001.patch`
## Impact
Called once per TLS handshake. With default configs (3–5 ciphers) the cost is trivial,
but the structure scales to O(n²) with `--data-ciphers` lists and adversarial peer
`IV_CIPHERS` advertisement. Each call also performs unnecessary heap allocation.

View file

@ -0,0 +1,66 @@
# otel-collector-0001: pcommon.Map Put* methods — O(n) Get inside O(n) construction loop
**Target:** OpenTelemetry Collector (pdata/pcommon)
**File:** `pdata/pcommon/map.go`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** HIGH
**Status:** PATCHED
## Description
`pcommon.Map` is backed by a `[]internal.KeyValue` slice (not a hash map). Every
mutation method — `PutStr`, `PutInt`, `PutDouble`, `PutBool`, `PutEmpty`,
`PutEmptyBytes`, `PutEmptyMap`, `PutEmptySlice`, `GetOrPutEmpty` — begins by
calling `m.Get(k)` to check for an existing key. `Get` iterates the entire slice
linearly:
```go
// map.go line 65-73
func (m Map) Get(key string) (Value, bool) {
for i := range *m.getOrig() {
akv := &(*m.getOrig())[i]
if akv.Key == key {
return newValue(&akv.Value, m.getState()), true
}
}
return newValue(nil, m.getState()), false
}
// map.go line 138-147 (PutStr — same pattern in all Put* methods)
func (m Map) PutStr(k, v string) {
if av, existing := m.Get(k); existing { // O(N) scan
av.SetStr(v)
return
}
// ... append new entry
}
```
When pipeline processors, exporters, or the resource-detection subsystem construct
or copy a span/metric/log attribute map with N key-value pairs by calling `PutStr`/
`PutInt`/… N times, the total cost is **O(N²)**: the k-th Put scans k existing
entries.
This affects every OTLP pipeline that manipulates attributes — resource enrichment,
attribute processors, batch partitioning, and debug exporters. Attribute maps with
30+ keys (typical for enriched telemetry) spend the majority of their construction
time in linear probes.
## Fix
Replace the underlying storage with `map[string]*internal.KeyValue` or maintain a
parallel `map[string]int` index into the slice. All `Get`/`Put` operations become
O(1). The existing `Range`/`All` iteration API remains unchanged since the slice
(or a stable ordering derived from it) can still be traversed.
Alternatively, accept the slice representation (required for protobuf compatibility)
but add a lazy `map[string]int` cache on first `Get`, invalidated on structural
mutations (`Remove`, `RemoveIf`, `EnsureCapacity`).
## Patch
`defects/otel-collector/patch/otel-collector-0001.patch`
## Unit Test
`defects/otel-collector/unit/OtelCollectorTest.java`

View file

@ -0,0 +1,66 @@
# ovs-0001: dpif-offload dpif_offload_port_add() — O(P) linear provider scan per port-add
## Status
PATCHED
## Target
openvswitch/ovs
## File
`lib/dpif-offload.c`
## Symptom
`dpif_offload_port_add()` is called for every port added to a datapath. When the
`pmd-rxq-affinity` configuration includes a provider priority list, the function executes a
`strtok_r` outer loop over priority tokens and an inner `LIST_FOR_EACH` over
`collection->list` — comparing each provider name via `strcmp`. With T priority tokens and
P offload providers, this is O(T × P) per port. In deployments with 200+ ports and
multiple SR-IOV offload providers, this executes tens of thousands of strcmp calls
during bring-up.
Additionally, `provider_collection_add()` (called from `move_provider_to_collection`)
performs a full O(P) linear scan through the collection list for duplicate detection.
## Root cause
```c
// dpif-offload.c:580-595
for (char *name = strtok_r(tokens, ",", &saveptr);
name;
name = strtok_r(NULL, ",", &saveptr)) {
LIST_FOR_EACH (offload, dpif_list_node, &collection->list) {
if (!strcmp(name, offload->class->type)) { // O(P) per token
...
}
}
}
```
And:
```c
// dpif-offload.c:229-234
LIST_FOR_EACH (offload_entry, dpif_list_node, providers_list) {
if (offload_entry == offload || !strcmp(offload->name,
offload_entry->name)) {
return EEXIST;
}
}
```
Both use linked list traversal for name-based membership tests.
## Fix
Build a `shash` (string hash map) of providers keyed by type name at collection creation
time. The priority-sort loop and the duplicate-check in `provider_collection_add` can then
use `shash_find()` — O(1) average — instead of list scan.
## Complexity
| Before | After |
|--------|-------|
| O(T × P) per port-add | O(T) per port-add |
## Patch
`defects/ovs/patch/ovs-0001-dpif-offload-port-add-linear-provider-scan.patch`
## Unit test
`defects/ovs/unit/OvsOffloadProviderTest.java`

View file

@ -0,0 +1,65 @@
# perl5-0001: S_pad_findlex() O(n) linear scan per lexical variable lookup
**Target:** Perl/perl5
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `pad.c` — `S_pad_findlex()` (~line 1168), called from `pad_findmy_pvn()`
**Status:** PATCHED
## Description
Every lexical variable reference at compile time goes through `pad_findmy_pvn()`
→ `S_pad_findlex()`. `S_pad_findlex()` performs a reverse linear scan from
`PadnamelistMAXNAMED` down to 1, comparing each pad name's length and pointer
against the target name. In a scope with N declared variables, each lookup is
O(N). A source file with M variable references and N variables in scope
performs O(M × N) pad-name comparisons at compile time.
Generated code (ORM layers, template engines, eval-heavy frameworks like
Dancer2/Mojolicious) can have hundreds of lexicals in scope and thousands of
references per compilation unit. A module with 500 `my` variables and 10,000
variable references performs 5,000,000 comparisons at `use` time.
## Root cause
```c
/* pad.c ~1168 — called for every lexical variable reference */
for (offset = PadnamelistMAXNAMED(names); offset > 0; offset--) {
const PADNAME * const name = name_p[offset];
if (name && PadnameLEN(name) == namelen
&& ( PadnamePV(name) == namepv /* pointer eq fast path */
|| memEQ(PadnamePV(name), namepv, namelen) ))
{
if (PadnameOUTER(name)) { fake_offset = offset; continue; }
if (PadnameIN_SCOPE(name, seq)) break;
}
}
```
The fast path (`PadnamePV(name) == namepv`) fires for interned strings in the
same compilation unit, but the loop still walks all N pad slots.
## Fix
Add a hash map (`padname_string → list of pad offsets`) to `PADNAMELIST`,
maintained by `padnamelist_store()`. `S_pad_findlex()` hashes the name once,
then iterates only the offsets that share that hash bucket (typically 1–3
entries), reducing the scan to O(1) amortised.
```c
/* In padnamelist_store() — O(1) extra work per declaration */
hv_store(padnamelist->pnl_hash, namepv, namelen,
newSVuv(offset), 0);
/* In S_pad_findlex() — replace linear scan with hash lookup */
SV **bucket = hv_fetch(padnamelist->pnl_hash, namepv, namelen, 0);
if (bucket) {
offset = (PADOFFSET)SvUV(*bucket);
/* verify scope with PadnameIN_SCOPE */
}
```
## Ops numbers (Java benchmark)
See `defects/perl5/unit/Perl5Test.java` (bench label "pad-findlex").
At N=500 pad names, M=2000 lookups: slow ~500,000 comparisons, fast ~2,000 → 250× speedup.

View file

@ -0,0 +1,65 @@
# php-0001 — zend_compile.c: O(n²) linear scan for named argument position
| Field | Value |
|-------|-------|
| ID | php-0001 |
| Target | PHP |
| File | `Zend/zend_compile.c` |
| Lines | 3755–3766, 3784, 3822 |
| CWE | CWE-407 (Algorithmic Complexity) |
| Severity | HIGH |
| Status | PATCHED |
## Description
`zend_get_arg_num()` performs a linear scan over `fn->common.num_args` entries
to locate a named argument by string comparison:
```c
static uint32_t zend_get_arg_num(const zend_function *fn, const zend_string *arg_name) {
// TODO: Caching?
for (uint32_t i = 0; i < fn->common.num_args; i++) {
zend_arg_info *arg_info = &fn->op_array.arg_info[i];
if (zend_string_equals(arg_info->name, arg_name)) {
return i + 1;
}
}
return (uint32_t) -1;
}
```
This is called from `zend_compile_args()` which iterates over all `args->children`
at compile time. For a function with M parameters called with N named arguments
the total cost is O(N × M). The upstream comment `// TODO: Caching?` acknowledges
the defect.
The companion runtime function `zend_get_arg_offset_by_name()` in
`zend_execute.c` (line 5479) carries the same structure with an explicit
`// TODO: Use a hash table?` comment, and is subject to identical quadratic
behavior on cache miss.
## Reproduction
```php
// Function with 50 named parameters, called with all 50 named — 50×50 = 2500 scans
function wide(
$p0, $p1, $p2, ..., $p49
) {}
// Each named arg triggers zend_get_arg_num linear scan over 50 entries
wide(p49: 1, p48: 2, ..., p0: 50);
```
## Fix
Build a `HashTable` from `arg_name → position` once per function signature and
cache it on the `zend_function` (or use the existing per-opcode cache slot for
the runtime path, which already has infrastructure but is not used for the
compile-time path).
## Complexity
| Metric | Before | After |
|--------|--------|-------|
| Per named-arg lookup | O(M) | O(1) |
| N named args, M params | O(N×M) | O(N + M) |
| Speedup (N=M=50) | 1× | ~50× |

View file

@ -0,0 +1,48 @@
# php-0002 — zend_execute.c: O(n²) linear scan for named argument offset at runtime
| Field | Value |
|-------|-------|
| ID | php-0002 |
| Target | PHP |
| File | `Zend/zend_execute.c` |
| Lines | 5477–5491 |
| CWE | CWE-407 (Algorithmic Complexity) |
| Severity | HIGH |
| Status | PATCHED |
## Description
`zend_get_arg_offset_by_name()` resolves a named argument to its positional
offset at call time. It uses a per-opcode cache slot, but on cache miss (first
call or when the function pointer changes) it falls back to a linear scan:
```c
// TODO: Use a hash table?
uint32_t num_args = fbc->common.num_args;
for (uint32_t i = 0; i < num_args; i++) {
const zend_arg_info *arg_info = &fbc->common.arg_info[i];
if (zend_string_equals(arg_name, arg_info->name)) {
...
return i;
}
}
```
Every unique call site × function-pointer combination incurs O(M) on first use.
In JIT-warmed code or long-running scripts that call many different functions
with named args this accumulates to O(N × M) cost. The upstream comment
`// TODO: Use a hash table?` explicitly acknowledges the defect.
## Fix
See php-0001. Build a per-function-signature hash mapping name → index once
and reuse across all call sites. The cache-slot mechanism already exists for
the "already resolved" fast path — extending it to populate a shared per-`fbc`
table avoids the O(M) fallback entirely.
## Complexity
| Metric | Before | After |
|--------|--------|-------|
| Per lookup (cache miss) | O(M params) | O(1) |
| N call sites × M params | O(N×M) | O(M + N) |

View file

@ -0,0 +1,47 @@
# pip-0001 — SimpleWheelCache: support_index_min() linear tag scan inside wheel candidate loop
## Target
`pypa/pip` — `src/pip/_internal/cache.py`
## CWE
CWE-407: Algorithmic Complexity — O(n) linear membership inside a hot loop
## Location
```
cache.py:160
wheel.support_index_min(supported_tags),
```
## Description
`SimpleWheelCache.get_path_for_link()` iterates over all cached wheel candidates and for each calls
`wheel.support_index_min(supported_tags)`. The `support_index_min()` method linearly enumerates the
`supported_tags` list (`list[Tag]`, typically 200–600 entries on CPython for multi-arch builds) to
find the lowest index where any of the wheel's file tags match.
Result: O(|candidates| × |supported_tags|) per cache lookup.
The fix used in `package_finder.py` (lines 474–479) — precomputing a `dict[Tag, int]` priority map
and using `find_most_preferred_tag()` — already exists in the codebase but was never applied to the
cache path. The cache path still calls the legacy O(n) `support_index_min`.
## Complexity
- Slow path: O(C × T) where C = cached wheel candidates, T = supported tags (~200-600)
- Fast path: O(C) with precomputed `tag_to_priority: dict[Tag, int]`
## Evidence
`package_finder.py:477`:
```python
self._wheel_tag_preferences = {
tag: idx for idx, tag in enumerate(supported_tags)
}
```
This exact fix pattern is adjacent but never applied to the cache code path.
## Patch
`defects/pip/patch/pip-0001-cache-support-index-min-precomputed-dict.patch`
## Unit Test
`defects/pip/unit/PipCacheTagScanTest.java`
## Status
PATCHED

View file

@ -0,0 +1,47 @@
# prometheus-0001: Builder.Labels() del-slice membership — slices.Contains O(n²)
**Target:** Prometheus (model/labels — slicelabels build tag)
**File:** `model/labels/labels_slicelabels.go`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** HIGH
**Status:** PATCHED
## Description
`Builder.Labels()` in the `slicelabels` implementation iterates every base label
and calls `slices.Contains(b.del, l.Name)` for each one. `b.del` is a `[]string`;
`slices.Contains` is O(D) where D = number of deleted labels. The outer loop is
O(L) where L = total base labels. Combined cost: **O(L × D)**.
The same path also calls `contains(b.add, l.Name)` (a hand-written linear scan of
`[]Label`), adding another O(L × A) term for A = added labels.
This function is the exit of every relabel rule application. The relabeling hot
path — `ProcessBuilder` in `model/relabel/relabel.go` — is called for every scrape
target on every scrape interval. For a `LabelDrop`/`LabelKeep` rule that deletes
K labels, D grows to K and the total cost per target per scrape is O(L²).
```go
// labels_slicelabels.go line 424-428
for _, l := range b.base {
if slices.Contains(b.del, l.Name) || contains(b.add, l.Name) {
continue // slices.Contains = O(D), called O(L) times → O(L×D)
}
res = append(res, l)
}
```
## Fix
Convert `b.del` to `map[string]struct{}` at construction (or lazily when first used)
so membership tests are O(1). The `contains(b.add, l.Name)` helper should similarly
use a `map[string]int` index into `b.add`. Total cost of `Builder.Labels()` drops to
O(L + D + A).
## Patch
`defects/prometheus/patch/prometheus-0001.patch`
## Unit Test
`defects/prometheus/unit/PrometheusTest.java`

View file

@ -0,0 +1,64 @@
# r-source-0001: rapply() do_one() nested loop class matching → O(k²)
**Target:** wch/r-source
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/main/apply.c` — `do_one()` (~line 312)
**Status:** PATCHED
## Description
The `rapply()` built-in applies a function recursively to elements matching a
set of classes. For each non-list element, `do_one()` checks whether the
element's class vector intersects with the user-supplied `classes` vector using
two nested linear loops: `for i over klass` × `for j over classes`. Both
vectors can be of length k (e.g. 10–20 for complex S4 hierarchies). The cost
per element is O(k²).
For a deeply nested list with N elements each having k classes checked against
k target classes, total cost is O(N × k²). In practice `rapply()` is called on
data frames and nested lists with dozens of columns, each having S3/S4 class
vectors. The nested scan fires once per element per recursive call.
## Root cause
```c
/* src/main/apply.c ~312 */
PROTECT(klass = R_data_class(X, false));
for(int i = 0; i < LENGTH(klass); i++) /* O(k) over element's classes */
for(int j = 0; j < length(classes); j++) /* O(k) over target classes */
if(Seql(STRING_ELT(klass, i), STRING_ELT(classes, j)))
matched = true;
UNPROTECT(1);
```
`Seql` compares two `CHARSXP` pointers; when the strings are interned it is
O(1), but the outer double loop is still O(k²) iterations regardless.
## Fix
Build a hash set from `classes` before recursing into the list, then check
membership with a single O(k) loop over `klass`. In C this can be done with a
small `STRSXP`-keyed hash via `Rf_installChar` / pointer comparison after
interning, or by using `R_StringHash`:
```c
/* Build a set of class name pointers (interned, so pointer-comparable) */
for(int j = 0; j < length(classes); j++)
interned[j] = Rf_installChar(STRING_ELT(classes, j));
for(int i = 0; i < LENGTH(klass); i++) {
SEXP ci = Rf_installChar(STRING_ELT(klass, i));
for(int j = 0; j < length(classes); j++)
if(ci == interned[j]) { matched = true; break; }
if(matched) break;
}
```
For k ≤ ~16 this is already a significant win (early-exit on first match +
pointer comparison). For larger k, a `HTAB`/`StringSet` gives O(k) total.
## Ops numbers (Java benchmark)
See `defects/r-source/unit/RSourceTest.java` (bench label "rapply-class-match").
At N=500 elements, k=20 classes: slow ~100,000 comparisons, fast ~10,000 → 10× speedup.

View file

@ -0,0 +1,46 @@
# rabbitmq-0003 — check_declare_arguments: O(n²) lists:member inside lists:filter
**Target:** rabbitmq/rabbitmq-server
**File:** `deps/rabbit/src/rabbit_amqqueue.erl`
**Functions:** `check_declare_arguments/3`, `check_consume_arguments/3`
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic Membership Test)
**Status:** PATCHED
## Defect
`check_declare_arguments` and `check_consume_arguments` both call:
```erlang
Validators = lists:filter(fun({Arg, _}) -> lists:member(Arg, QueueTypeArgs) end, declare_args()),
```
`declare_args()` returns a list of ~21 argument validators. `QueueTypeArgs` is also a list.
`lists:filter` iterates over all validators (O(D)) and for each element calls `lists:member`
against `QueueTypeArgs` (O(Q)). Total: **O(D × Q)** per queue declare/consume operation.
With type arguments lists growing as new queue types are added, this degrades proportionally.
## Fix
Convert `QueueTypeArgs` to an Erlang `sets` (or `gb_sets`) before the filter. `sets:is_element`
is O(1) average, reducing the filter to O(D).
```erlang
% Before
Validators = lists:filter(fun({Arg, _}) -> lists:member(Arg, QueueTypeArgs) end, declare_args()),
% After
QueueTypeArgsSet = sets:from_list(QueueTypeArgs),
Validators = lists:filter(fun({Arg, _}) -> sets:is_element(Arg, QueueTypeArgsSet) end, declare_args()),
```
Same fix applied to `check_consume_arguments`.
## Patch
`defects/rabbitmq/patch/rmq-0003-check-declare-args-sets.patch`
## Unit Test
`defects/rabbitmq/unit/RabbitMQQueueTest.java` (extended with rabbitmq-0003 case)

View file

@ -0,0 +1,52 @@
# rabbitmq-0004 — check_arguments_key: O(n²) lists:member(ArgKey, InvalidArgs) in lists:foreach
**Target:** rabbitmq/rabbitmq-server
**File:** `deps/rabbit/src/rabbit_amqqueue.erl`
**Function:** `check_arguments_key/4`
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic Membership Test)
**Status:** PATCHED
## Defect
```erlang
check_arguments_key(QueueName, QueueType, Args, InvalidArgs) ->
lists:foreach(fun(Arg) ->
ArgKey = element(1, Arg),
case lists:member(ArgKey, InvalidArgs) of
...
end
end, Args).
```
`Args` is the full set of arguments supplied by the client. `InvalidArgs` is a list derived from
`rabbit_queue_type:arguments(queue_arguments) -- QueueTypeArgs`. For each element of `Args` (O(A)),
`lists:member` scans `InvalidArgs` (O(I)). Total: **O(A × I)** per queue declare/consume call.
## Fix
Convert `InvalidArgs` to a set before the loop:
```erlang
check_arguments_key(QueueName, QueueType, Args, InvalidArgs) ->
InvalidArgsSet = sets:from_list(InvalidArgs),
lists:foreach(fun(Arg) ->
ArgKey = element(1, Arg),
case sets:is_element(ArgKey, InvalidArgsSet) of
false -> ok;
true ->
rabbit_misc:protocol_error(
precondition_failed,
"invalid arg '~ts' for ~ts of queue type ~ts",
[ArgKey, rabbit_misc:rs(QueueName), QueueType])
end
end, Args).
```
## Patch
`defects/rabbitmq/patch/rmq-0004-check-arguments-key-sets.patch`
## Unit Test
`defects/rabbitmq/unit/RabbitMQQueueTest.java` (extended with rabbitmq-0004 case)

View file

@ -0,0 +1,68 @@
# raylib-0001 — GetGlyphIndex O(n) linear scan per character per DrawText call
## Status
PATCHED
## Severity
HIGH
## Target
raylib `src/rtext.c`
## CWE
CWE-407: Algorithmic Complexity — Insufficient Complexity Reduction Before Algorithmic
Intensive Operation
## Description
`GetGlyphIndex(Font font, int codepoint)` performs an O(n) linear scan of
`font.glyphs[]` for every character in every text draw call (DrawText, DrawTextEx,
MeasureText, MeasureTextEx, DrawTextCodepoint, etc.). For a font with N glyphs,
rendering a string of L characters costs O(L * N) per frame.
In the `SUPPORT_UNORDERED_CHARSET` branch (enabled by default via `#define`), every
call walks the full glyphs array until it finds the matching codepoint or exhausts the
array.
```c
// rtext.c:1453
int GetGlyphIndex(Font font, int codepoint)
{
int index = 0;
...
for (int i = 0; i < font.glyphCount; i++) // O(n) every call
{
if (font.glyphs[i].value == 63) fallbackIndex = i;
if (font.glyphs[i].value == codepoint) { index = i; break; }
}
...
}
```
## Call Sites (partial)
- `DrawText` → `DrawTextEx` → inner loop calls `GetGlyphIndex` per codepoint
- `MeasureTextEx` → inner loop calls `GetGlyphIndex` per codepoint
- `DrawTextCodepoints` → inner loop
- `GetGlyphInfo` → `GetGlyphIndex`
- `GetGlyphAtlasRec` → `GetGlyphIndex`
## Fix
Build a `codepoint → index` hash map (or sorted array + binary search) once at font
load time. `GetGlyphIndex` then becomes O(1) average (hash map) or O(log n) (binary
search on sorted glyphs).
Patch: sort glyphs by codepoint value at load time and use binary search. This keeps
memory layout unchanged and requires no new heap allocation.
## Complexity
- Before: O(L * N) per DrawText call where L = string length, N = font.glyphCount
- After: O(L * log N) with binary search; O(L) with hash map
## Benchmark
See `defects/raylib/unit/RaylibGlyphIndexTest.java` — at N=2048 glyphs:
- slow() (linear scan): ~2048 comparisons per lookup
- fast() (binary search): ~11 comparisons per lookup
- Ratio: ~186x
## Files
- `defects/raylib/patch/raylib-0001.patch`
- `defects/raylib/unit/RaylibGlyphIndexTest.java`

View file

@ -0,0 +1,68 @@
# redis-0001: SINTER/SDIFF on listpack sets — O(N×M) quadratic membership
**Target:** redis/redis
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/t_set.c:1472` (`sinterGenericCommand`)
**Status:** PATCHED
## Description
`sinterGenericCommand` iterates every element of the smallest set (outer loop, N
elements) and for each element calls `setTypeIsMemberAux` on each of the
remaining sets. When those sets use `OBJ_ENCODING_LISTPACK` (the default for
sets with ≤ 128 entries, configured via `set-max-listpack-entries`),
`setTypeIsMemberAux` dispatches to `lpFind`, which is an O(M) linear scan
through the listpack blob.
Result: O(N × M) per intersection query instead of O(N) with a hash table.
At the default threshold of 128 entries:
- Slow (listpack): 128 × 128 = 16,384 comparisons
- Fast (hash table): 128 × O(1) = 128 comparisons
- Worst-case speedup: **128×**
The same quadratic path exists in `sunionDiffGenericCommand` DIFF algorithm 1
(comment in source confirms "O(N*M) where N is the size of the first set and M
the number of sets") when inner sets are listpack-encoded.
## Hot paths
- `SINTER key1 key2 [...]` — intersection query
- `SINTERCARD numkeys key1 key2 [LIMIT n]` — cardinality of intersection
- `SINTERSTORE dst key1 key2 [...]` — store intersection
- `SDIFF key1 key2` (algo 1) — difference query when sets are small
## Root cause
`src/t_set.c:1474–1477`:
```c
while((encoding = setTypeNext(&si, &str, &len, &intobj)) != -1) {
for (j = 1; j < setnum; j++) {
if (!setTypeIsMemberAux(sets[j].set, str, len, intobj,
encoding == OBJ_ENCODING_HT))
```
`setTypeIsMemberAux` at encoding `OBJ_ENCODING_LISTPACK` calls `lpFind`, which
walks the packed byte array from the beginning — O(M).
## Fix
Before entering the intersection loop, convert any listpack-encoded set to a
temporary hash table (`dictCreate`) so that subsequent membership checks are
O(1). The temporary dict is freed after the loop. Sets already using
`OBJ_ENCODING_HT` or `OBJ_ENCODING_INTSET` are left untouched.
See patch: `defects/redis/patch/0001-sinter-listpack-promote-to-htset.patch`
## Also affects
- valkey/valkey — identical `sinterGenericCommand` / `sunionDiffGenericCommand`
code paths.
## Ops ratio (unit test)
N=128, M=128:
- slow ops: 16,384
- fast ops: 128
- speedup: **128×**

View file

@ -0,0 +1,60 @@
# redis-0002: getUpcomingChannelList O(S×C²) quadratic channel membership
**Target:** redis/redis
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/acl.c:1952` (`getUpcomingChannelList`)
**Status:** PATCHED
## Description
`getUpcomingChannelList(user *new, user *original)` builds a flat linked list
(`upcoming`) of all channel patterns from `new`'s selectors, then for each
channel pattern in `original`'s selectors calls `listSearchKey(upcoming, ...)`.
`listSearchKey` is an O(n) walk of the `upcoming` list. With S selectors and C
channel patterns per selector the `upcoming` list grows to S×C entries.
The outer loop also iterates S×C patterns.
Result: O((S×C)²) in the worst case — quadratic in total channel-pattern count.
In practice, heavily compartmentalized users (many selectors, many channel ACL
rules) hit this during `ACL SETUSER` and at any `SUBSCRIBE`/`PSUBSCRIBE` event
that triggers client-kill evaluation.
## Hot paths
- `ACL SETUSER` — triggers `killPubsubClientsIfNeeded`, which calls
`getUpcomingChannelList` for each affected client.
- `SUBSCRIBE` / `PSUBSCRIBE` evaluation when ACL rules restrict channels.
## Root cause
`src/acl.c:1935–1960`:
```c
list *upcoming = listCreate(); // O(S×C) entries
...
while((lpn = listNext(&lpi)) && match) {
if (!listSearchKey(upcoming, listNodeValue(lpn))) { // O(S×C) per call
match = 0;
```
## Fix
Replace the `upcoming` linked list with a hash table (`dictCreate` with
`dictTypeSds` or a simple `rax`). Build the set from `new`'s selectors, then
check each of `original`'s channel patterns with O(1) lookup. Total cost
becomes O(S×C) instead of O((S×C)²).
See patch: `defects/redis/patch/0002-acl-upcoming-channels-dict.patch`
## Also affects
- valkey/valkey — identical `getUpcomingChannelList` code path.
## Ops ratio (unit test)
S=4 selectors, C=50 channels each (200 total):
- slow ops: 200 × 200 = 40,000
- fast ops: 200 (one pass build) + 200 (O(1) lookups) = 400
- speedup: **100×**

View file

@ -0,0 +1,76 @@
# ruby-0001 — vm_args.c: O(n²) linear keyword-argument matching per call
| Field | Value |
|-------|-------|
| ID | ruby-0001 |
| Target | Ruby |
| File | `vm_args.c` |
| Lines | 300–374 |
| CWE | CWE-407 (Algorithmic Complexity) |
| Severity | HIGH |
| Status | PATCHED |
## Description
`args_setup_kw_parameters()` maps passed keyword arguments to formal parameter
slots. For each of the function's `key_num` acceptable keywords it calls
`args_setup_kw_parameters_lookup()`, which linearly scans the full
`passed_keyword_len` array of passed keyword names:
```c
static inline int
args_setup_kw_parameters_lookup(const ID key, VALUE *ptr,
const VALUE *const passed_keywords, VALUE *passed_values,
const int passed_keyword_len)
{
int i;
const VALUE keyname = ID2SYM(key);
for (i=0; i<passed_keyword_len; i++) { // O(passed_keyword_len)
if (keyname == passed_keywords[i]) {
...
return TRUE;
}
}
return FALSE;
}
```
Called from two loops in `args_setup_kw_parameters`:
```c
for (i=0; i<req_key_num; i++) {
args_setup_kw_parameters_lookup(acceptable_keywords[i], ...); // O(P)
}
for (di=0; i<key_num; i++, di++) {
args_setup_kw_parameters_lookup(acceptable_keywords[i], ...); // O(P)
}
```
Total cost: O(key_num × passed_keyword_len). For a method with K keyword
params called with P keyword arguments, every call is O(K × P).
## Reproduction
```ruby
# Method with 20 keyword params, called with all 20 named
def wide(a0:, a1:, a2:, a3:, a4:, a5:, a6:, a7:, a8:, a9:,
b0:, b1:, b2:, b3:, b4:, b5:, b6:, b7:, b8:, b9:) end
# Each call: 20 acceptable keywords × 20 passed = 400 linear comparisons
# vs hash: 20 + 20 = 40 operations
```
## Fix
Build a temporary `st_table` (Ruby's hash) from `passed_keywords` → index
once before the two loops, then replace `args_setup_kw_parameters_lookup` with
an O(1) `st_lookup`. Build cost: O(P). Total: O(K + P).
## Complexity
| Metric | Before | After |
|--------|--------|-------|
| Per call (K params, P passed) | O(K × P) | O(K + P) |
| K=P=20 | 400 comparisons | 40 ops |
| Speedup (K=P=20) | 1× | ~10× |
| K=P=100 | 10 000 comparisons | 200 ops |
| Speedup (K=P=100) | 1× | ~50× |

View file

@ -0,0 +1,57 @@
# scala-0001 — CWE-407: Checkable propagateKnownTypes baseClasses O(n²) nested scan
**Target:** scala/scala — `src/compiler/scala/tools/nsc/typechecker/Checkable.scala`
**Severity:** HIGH
**Complexity:** O(n²) → O(n)
## Defect
`propagateKnownTypes` (Checkable.scala:98) performs a nested O(n²) scan:
```scala
from.baseClasses foreach { bc => if (to.baseClasses.contains(bc)){
```
Both `from.baseClasses` and `to.baseClasses` return `List[Symbol]`. The outer
`foreach` iterates over `from.baseClasses` (length M), and for each element calls
`to.baseClasses.contains(bc)` which is O(N) linear scan. Total: O(M×N).
In Scala's type system, class hierarchy depth determines M and N. For typical
Scala projects this is 10–50 base classes per type; for framework-heavy code
(Akka, Play, Cats) it can reach 100+. The outer loop is called during pattern
match type-checking for every `match`/`isInstanceOf` in source.
`propagateKnownTypes` is called from `CheckabilityChecker` during `Typers`
pattern match elaboration — a hot path for Scala programs that use `match`
extensively.
## Fix
Convert `to.baseClasses` to a `Set` before the loop:
```scala
def propagateKnownTypes(from: Type, to: Symbol): Type = {
def tparams = to.typeParams
val tvars = tparams map (p => TypeVar(p))
val tvarType = appliedType(to, tvars)
val toBaseClassSet = to.baseClasses.toSet // O(n) once
from.baseClasses foreach { bc => if (toBaseClassSet.contains(bc)){
```
`Symbol` implements `hashCode`/`equals` by identity (object reference), so
`Set[Symbol]` gives O(1) membership. The one-time conversion cost is O(N) and
the outer loop becomes O(M) total.
## Impact
- Every pattern match expression in Scala source triggers `propagateKnownTypes`
- Deep class hierarchies (trait-heavy Scala libraries) magnify the defect
- O(n²) in hierarchy depth per match arm, multiplied by number of match expressions
- Compile time for large Scala codebases (Spark, Akka, Cats) is meaningfully
affected — particularly files with many `match` expressions over typed hierarchies
## Files
- Patch: `defects/scala/patch/scala-0001.patch`
- Unit: `defects/scala/unit/ScalaCheckableTest.java`

View file

@ -0,0 +1,58 @@
# sdl2-0001 — SDL_GetJoystickFromID / SDL_GetGamepadFromID O(n) linked-list walk
## Status
PATCHED
## Severity
MEDIUM
## Target
SDL `src/joystick/SDL_joystick.c`, `src/joystick/SDL_gamepad.c`
## CWE
CWE-407: Algorithmic Complexity — Insufficient Complexity Reduction Before Algorithmic
Intensive Operation
## Description
`SDL_GetJoystickFromID(SDL_JoystickID instance_id)` and
`SDL_GetGamepadFromID(SDL_JoystickID joyid)` both perform O(n) linked-list walks to
find a joystick/gamepad by its instance ID.
```c
// SDL_joystick.c:1960
SDL_Joystick *SDL_GetJoystickFromID(SDL_JoystickID instance_id)
{
SDL_LockJoysticks();
for (joystick = SDL_joysticks; joystick; joystick = joystick->next) { // O(n)
if (joystick->instance_id == instance_id) break;
}
SDL_UnlockJoysticks();
return joystick;
}
// SDL_gamepad.c:4160
SDL_Gamepad *SDL_GetGamepadFromID(SDL_JoystickID joyid)
{
while (gamepad) { // O(n)
if (gamepad->joystick->instance_id == joyid) ...
gamepad = gamepad->next;
}
}
```
These functions are called 41+ times across `src/joystick/` including per-packet HIDAPI
device update loops (SDL_hidapijoystick.c:652, :808, :873), per-device-num-joysticks
inner loops, and device property update paths. With N open joysticks, each call is O(N).
## Fix
Add a `SDL_HashTable` (SDL already has `SDL_hashtable.h`) mapping
`SDL_JoystickID → SDL_Joystick*` and `SDL_JoystickID → SDL_Gamepad*`.
Insert on open, remove on close. Lookup becomes O(1).
## Complexity
- Before: O(n) per lookup where n = number of open joysticks
- After: O(1) with hash table
## Files
- `defects/sdl2/patch/sdl2-0001.patch`
- `defects/sdl2/unit/Sdl2JoystickLookupTest.java`

View file

@ -0,0 +1,70 @@
# spring-0003: AbstractApplicationEventMulticaster.retrieveApplicationListeners — ArrayList.contains() O(n²)
**Target:** Spring Framework
**File:** `spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** HIGH
**Status:** PATCHED
## Description
`retrieveApplicationListeners()` builds `allListeners` as `ArrayList<ApplicationListener<?>>`.
It then loops over `listenerBeans` (O(L) beans), and inside that loop calls
`allListeners.contains()` twice — at lines 279 and 285:
```java
// AbstractApplicationEventMulticaster.java line 236, 262-295
List<ApplicationListener<?>> allListeners = new ArrayList<>(); // ArrayList!
for (String listenerBeanName : listenerBeans) { // O(L) iterations
...
if (allListeners.contains(unwrappedListener)) { // O(n) scan — line 279
allListeners.remove(unwrappedListener);
allListeners.add(listener);
}
if (!allListeners.contains(listener) && ...) { // O(n) scan — line 285
allListeners.add(listener);
}
}
```
`allListeners` is populated first from `listeners` (O(P) programmatic listeners),
so at line 262 the list already has up to P entries. Each of the L bean-name
lookups performs up to two O(P+L) scans. Total: O(L × (P+L)) = O(n²) in the
worst case where L ≈ P ≈ n.
This path is hit on **every event dispatch** that is not fully cached —
including `ContextRefreshedEvent`, `ApplicationReadyEvent`, and any custom event
whose type has not been seen before. Large Spring Boot applications with many
listeners pay this tax on startup and every cache-miss dispatch.
## Fix
Replace `ArrayList` with `LinkedHashSet` for `allListeners` throughout
`retrieveApplicationListeners()`. `Set.add()` provides O(1) dedup, eliminating
both `contains()` calls. The `remove()`+`add()` pair for proxy replacement
reduces to `remove()`+`add()` on a Set (both O(1)).
```java
// Fixed
LinkedHashSet<ApplicationListener<?>> allListeners = new LinkedHashSet<>();
// ...
if (filteredListeners != null && filteredListeners.contains(unwrappedListener)) { ... }
// allListeners.contains() calls replaced by Set membership or direct add()
if (allListeners.add(listener) && supportsEvent(listener, eventType, sourceType)) { ... }
```
Convert back to `List` for the final sort:
```java
List<ApplicationListener<?>> sorted = new ArrayList<>(allListeners);
AnnotationAwareOrderComparator.sort(sorted);
return sorted;
```
## Patch
`defects/spring/patch/spring-0003-0004-eventmulticaster-linkedhashset.patch`
## Unit Test
`defects/spring/unit/SpringEventMulticasterTest.java`

View file

@ -0,0 +1,56 @@
# spring-0004: DefaultListenerRetriever.getApplicationListeners — ArrayList.contains() O(n²)
**Target:** Spring Framework
**File:** `spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`DefaultListenerRetriever.getApplicationListeners()` (inner class, line 496)
builds `allListeners` as `ArrayList` and then iterates `applicationListenerBeans`,
calling `allListeners.contains(listener)` inside the loop:
```java
// AbstractApplicationEventMulticaster.java line 503-521
List<ApplicationListener<?>> allListeners = new ArrayList<>(
this.applicationListeners.size() + this.applicationListenerBeans.size());
allListeners.addAll(this.applicationListeners); // up to P entries
for (String listenerBeanName : this.applicationListenerBeans) { // O(L) iterations
ApplicationListener<?> listener = beanFactory.getBean(...);
if (!allListeners.contains(listener)) { // O(P+i) scan — line 512
allListeners.add(listener);
}
}
```
Same pattern as spring-0003 but in the non-cached retriever path (used when the
default retriever's list is returned directly). O(L × P) = O(n²) in the
worst case.
## Fix
Use a `LinkedHashSet` for `allListeners` (insertion-ordered, O(1) dedup):
```java
LinkedHashSet<ApplicationListener<?>> allListeners = new LinkedHashSet<>(
this.applicationListeners);
for (String listenerBeanName : this.applicationListenerBeans) {
allListeners.add(beanFactory.getBean(listenerBeanName, ApplicationListener.class));
// Set.add() is O(1) and idempotent — no contains() guard needed
}
List<ApplicationListener<?>> sorted = new ArrayList<>(allListeners);
AnnotationAwareOrderComparator.sort(sorted);
return sorted;
```
## Patch
`defects/spring/patch/spring-0003-0004-eventmulticaster-linkedhashset.patch`
## Unit Test
`defects/spring/unit/SpringEventMulticasterTest.java`

View file

@ -0,0 +1,48 @@
# storm-0001: Fields constructor duplicate check — List.contains() O(n²)
**Target:** Apache Storm
**File:** `storm-client/src/jvm/org/apache/storm/tuple/Fields.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
The `Fields(List<String>)` constructor checks for duplicate field names using
`this.fields.contains(field)` where `fields` is an `ArrayList<String>`. The loop
over n fields each calls `contains()` on a growing ArrayList — worst-case O(n²)
for a fully-unique field list.
```java
// storm-client/.../tuple/Fields.java line 35-44
private List<String> fields; // ArrayList<String>
private Map<String, Integer> index = new HashMap<>();
public Fields(List<String> fields) {
this.fields = new ArrayList<>(fields.size());
for (String field : fields) {
if (this.fields.contains(field)) { // O(n) per iteration → O(n²) total
throw new IllegalArgumentException(
String.format("duplicate field '%s'", field));
}
this.fields.add(field);
}
index(); // builds the HashMap index AFTER the loop
}
```
`index` is built after construction. The fix is to use `index` (which is populated
incrementally) for the duplicate check, converting the O(n²) constructor to O(n).
## Fix
Check `index.containsKey(field)` instead of `this.fields.contains(field)` and add to
`index` inline. The existing `index()` call at the end becomes a no-op or is removed.
## Patch
`defects/storm/patch/storm-0001.patch`
## Unit Test
`defects/storm/unit/StormFieldsDedupTest.java`

View file

@ -0,0 +1,45 @@
# storm-0002: WorkerState topology reassignment — ArrayList.contains() O(n²)
**Target:** Apache Storm
**File:** `storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`WorkerState.refreshConnections()` iterates over all tasks in the assignment
(outer loop, O(n)) and calls `localTaskIds.contains(task)` where `localTaskIds`
is an `ArrayList<Integer>` (inner O(n) scan), yielding O(n²) per reassignment event.
```java
// storm-client/.../worker/WorkerState.java line 112 / 424-431
final ArrayList<Integer> localTaskIds; // O(n) contains
// in refreshConnections():
for (Map.Entry<Integer, NodeInfo> taskToNodePortEntry : taskToNodePort.entrySet()) {
Integer task = taskToNodePortEntry.getKey();
if (outboundTasks.contains(task)) { // outboundTasks is Set<Integer> — O(1)
newTaskToNodePort.put(task, taskToNodePortEntry.getValue());
if (!localTaskIds.contains(task)) { // ArrayList — O(n) ← defect
neededConnections.add(taskToNodePortEntry.getValue());
}
}
}
```
`outboundTasks` is already a `Set<Integer>` so that check is O(1). `localTaskIds` should
also be a `HashSet<Integer>` for the membership check.
## Fix
Maintain a parallel `Set<Integer> localTaskIdSet` (or convert `localTaskIds` to
`LinkedHashSet`) so that `localTaskIds.contains()` becomes O(1).
## Patch
`defects/storm/patch/storm-0002.patch`
## Unit Test
`defects/storm/unit/StormWorkerStateTaskLookupTest.java`

View file

@ -0,0 +1,56 @@
# tidb-0001: getEnforcedMergeJoin / getNewJoinKeys offsets slices.Contains — O(N²)
**Severity:** MEDIUM
**File:** pkg/planner/core/operator/physicalop/physical_merge_join.go
**Line:** 173, 513, 527
**Status:** PATCHED
## Description
Three functions use `slices.Contains(offsets, pos)` inside loops over join keys:
**`getEnforcedMergeJoin`** (line 158–197): iterates `prop.SortItems` (outer loop),
then iterates `leftJoinKeys` (inner loop), calling `slices.Contains(offsets, joinKeyPos)`
on each inner iteration. `offsets` grows as matches are found. With K sort items
and N join keys, this is O(K × N) comparisons in the inner body — and each
`slices.Contains` call is itself O(len(offsets)), making total work O(K × N²).
**`getNewJoinKeysByOffsets`** (line 508–518): iterates all `oldJoinKeys`, calling
`slices.Contains(offsets, pos)` for each — O(N × |offsets|) = O(N²) worst case.
**`getNewNullEQByOffsets`** (line 521–531): identical pattern to `getNewJoinKeysByOffsets`.
These functions run during physical plan enumeration for every Sort-Merge Join
candidate. Queries with 20+ join keys on wide tables trigger O(400) comparisons
where O(20) suffices.
## Root Cause
`offsets` is a `[]int` slice. Membership is checked with `slices.Contains`,
which does a linear scan. A `map[int]struct{}` or `[]bool` bitset (indexed by
join key position) reduces each lookup to O(1).
## Fix
Use a `map[int]struct{}` (or equivalently a `[]bool` sized to the number of
join keys) alongside `offsets`:
```go
// In getEnforcedMergeJoin:
offsets := make([]int, 0, len(leftJoinKeys))
offsetSet := make(map[int]struct{}, len(leftJoinKeys))
// ...
if _, exists := offsetSet[joinKeyPos]; !exists {
offsets = append(offsets, joinKeyPos)
offsetSet[joinKeyPos] = struct{}{}
}
// In getNewJoinKeysByOffsets / getNewNullEQByOffsets:
offsetSet := make(map[int]struct{}, len(offsets))
for _, o := range offsets { offsetSet[o] = struct{}{} }
// then: if _, exists := offsetSet[pos]; !exists { ... }
```
## Speedup
O(N²) → O(N). ~20x at N=20 join keys; ~100x at N=50.

View file

@ -0,0 +1,61 @@
# tidb-0002: mergeInAndNotEQLists removeValues slices.Contains — O(N²)
**Severity:** MEDIUM
**File:** pkg/planner/core/rule/rule_predicate_simplification.go
**Line:** 267
**Status:** PATCHED
## Description
`mergeInAndNotEQLists` simplifies NE/IN-list predicate pairs in the planner.
It builds `removeValues []int` containing predicate indices to remove, then
filters them out:
```go
// O(N²) inner loop builds removeValues
for i := range predicates {
for j := i + 1; j < len(predicates); j++ {
// ... may append i or j to removeValues
}
}
// O(N) loop with O(|removeValues|) check per iteration = O(N²) filter
newValues := make([]expression.Expression, 0, len(predicates))
for i, value := range predicates {
if !(slices.Contains(removeValues, i)) { // O(|removeValues|) scan
newValues = append(newValues, value)
}
}
```
The O(N²) inner double loop is structural (all pairs must be checked), but the
final filter adds an additional O(N × |removeValues|) pass where a bitset or
`map[int]struct{}` would make it O(N).
For a query with 100 IN-list predicates, the filter alone performs up to 10 000
index comparisons rather than 100.
## Root Cause
`removeValues` is a `[]int` slice used as a set. `slices.Contains` performs a
linear scan on every iteration of the output loop.
## Fix
Replace `removeValues []int` with `removeSet map[int]struct{}`:
```go
removeSet := make(map[int]struct{})
// ... replace: removeValues = append(removeValues, i/j)
// with: removeSet[i] = struct{}{} / removeSet[j] = struct{}{}
newValues := make([]expression.Expression, 0, len(predicates))
for i, value := range predicates {
if _, skip := removeSet[i]; !skip {
newValues = append(newValues, value)
}
}
```
## Speedup
Filter pass: O(N²) → O(N). At N=100 predicates: ~10 000 comparisons → ~100.

View file

@ -0,0 +1,62 @@
# tomcat-0001: ReplicationValve cross-context session dedup — ArrayList.contains() O(n²)
**Target:** Apache Tomcat
**File:** `java/org/apache/catalina/ha/tcp/ReplicationValve.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
`ReplicationValve.registerReplicationSession()` accumulates cross-context
`DeltaSession` objects in a `ThreadLocal<ArrayList<DeltaSession>>`. For each
incoming session it checks membership with `sessions.contains(session)` before
adding, where `sessions` is `ArrayList<DeltaSession>`.
In Portlet-style deployments the comment in the code explicitly notes that
"the Portlet API can include a lot of fragments from same or different
applications with session changes." Each call to `registerReplicationSession()`
does a full linear scan of the growing list — O(n) per call — producing
O(n²) total work when n sessions are registered for a single request.
```java
// ReplicationValve.java line 80, 265-275
protected final ThreadLocal<ArrayList<DeltaSession>> crossContextSessions = new ThreadLocal<>();
public void registerReplicationSession(DeltaSession session) {
List<DeltaSession> sessions = crossContextSessions.get();
if (sessions != null) {
if (!sessions.contains(session)) { // O(n) scan per call → O(n²) total
sessions.add(session);
}
}
}
```
## Fix
Replace `ArrayList<DeltaSession>` with `LinkedHashSet<DeltaSession>` in the
`ThreadLocal` initialiser. `Set.add()` is idempotent so the `contains()` guard
is unnecessary and can be removed entirely.
```java
protected final ThreadLocal<LinkedHashSet<DeltaSession>> crossContextSessions = new ThreadLocal<>();
public void registerReplicationSession(DeltaSession session) {
LinkedHashSet<DeltaSession> sessions = crossContextSessions.get();
if (sessions != null) {
sessions.add(session); // O(1) — Set deduplicates automatically
}
}
```
Callers that iterate `sessions` (lines 394, 422) are unaffected — `LinkedHashSet`
is `Iterable` and preserves insertion order.
## Patch
`defects/tomcat/patch/tomcat-0001.patch`
## Unit Test
`defects/tomcat/unit/TomcatReplicationValveTest.java`

View file

@ -0,0 +1,67 @@
# tor-0002: nodelist_add_node_and_family family-ID scan — O(N·F²) smartlist_contains_string
**Target:** Tor
**File:** `src/feature/nodelist/nodelist.c`
**Function:** `nodelist_add_node_and_family` / `nodes_have_common_family_id`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** HIGH
**Status:** PATCHED
## Description
`nodelist_add_node_and_family()` adds all relays that share a verified family ID
with a given node. The implementation iterates over every node in the consensus
(`all_nodes`, size N) and for each pair calls `nodes_have_common_family_id()`:
```c
// nodelist.c line 2337-2340
SMARTLIST_FOREACH(all_nodes, const node_t *, node2, {
if (nodes_have_common_family_id(node, node2)) {
smartlist_add(sl, (void *)node2);
}
});
```
`nodes_have_common_family_id()` itself is:
```c
// nodelist.c line 2190-2200
static bool
nodes_have_common_family_id(const node_t *a, const node_t *b)
{
const smartlist_t *ids_a = node_get_family_ids(a);
const smartlist_t *ids_b = node_get_family_ids(b);
if (ids_a == NULL || ids_b == NULL)
return false;
SMARTLIST_FOREACH(ids_a, const char *, id, {
if (smartlist_contains_string(ids_b, id)) // O(|ids_b|) linear scan
return true;
});
return false;
}
```
`smartlist_contains_string()` performs a full linear scan of `ids_b`.
Total cost: **O(N × |ids_a| × |ids_b|)** — equivalently O(N·F²) where N is
the consensus size (~7 000 relays) and F is family IDs per node. The function
is called from path selection during circuit building, which is latency-critical.
## Fix
Before the outer `SMARTLIST_FOREACH(all_nodes)` loop, build a string-keyed set
from `node`'s own family IDs (the fixed `ids_a`). Then for each `node2`, iterate
`node2`'s `ids_b` and perform O(1) lookups against the set. This reduces the
per-pair check to **O(|ids_b|)** and the total to **O(N·F)**.
The patch uses Tor's existing `strmap_t` (string-keyed hash map) as the set:
insert each id from `ids_a` once before the outer loop, then call
`strmap_get(id_set, id)` inside the inner loop.
## Patch
`defects/tor/patch/tor-0002-nodelist-family-id-strmap.patch`
## Unit Test
`defects/tor/unit/TorNodelistFamilyTest.java`

View file

@ -0,0 +1,63 @@
# tor-0003: scheduler_kist.c re-add loop — smartlist_contains(cp) O(|cp|) per channel
**Target:** Tor
**File:** `src/core/or/scheduler_kist.c`
**Function:** `kist_scheduler_run` (re-add section)
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** MEDIUM
**Status:** PATCHED
## Description
At the end of the KIST scheduler's main drain loop, channels that need to be
re-added to the pending priority queue are collected in `to_readd`. The guard
for re-insertion uses `smartlist_contains(cp, readd_chan)`:
```c
// scheduler_kist.c line 756-766
SMARTLIST_FOREACH_BEGIN(to_readd, channel_t *, readd_chan) {
scheduler_set_channel_state(readd_chan, SCHED_CHAN_PENDING);
if (!smartlist_contains(cp, readd_chan)) { // O(|cp|) scan
if (!SCHED_BUG(readd_chan->sched_heap_idx != -1, readd_chan)) {
/* XXXX Note that the check above is in theory redundant with
* the smartlist_contains check. */
smartlist_pqueue_add(cp, scheduler_compare_channels,
offsetof(channel_t, sched_heap_idx), readd_chan);
}
}
} SMARTLIST_FOREACH_END(readd_chan);
```
`smartlist_contains()` does a linear pointer scan of the entire pending-channel
list `cp`. The code comment already acknowledges that the inner
`sched_heap_idx != -1` check is "in theory redundant" with the O(|cp|) scan —
meaning the O(1) O(1) guard is already present but ordered *after* the O(n) one.
With T channels in `to_readd` and C channels in `cp`, the cost is **O(T·C)**.
During high-load periods both lists can hold hundreds of channels.
## Fix
Remove the `smartlist_contains(cp, readd_chan)` call entirely. Rely solely on
`sched_heap_idx != -1` as the O(1) membership test. A channel is already in `cp`
(the pqueue, backed by the same smartlist) if and only if its heap index is set;
`smartlist_pqueue_add` maintains this invariant.
```c
// After fix:
SMARTLIST_FOREACH_BEGIN(to_readd, channel_t *, readd_chan) {
scheduler_set_channel_state(readd_chan, SCHED_CHAN_PENDING);
if (readd_chan->sched_heap_idx == -1) {
smartlist_pqueue_add(cp, scheduler_compare_channels,
offsetof(channel_t, sched_heap_idx), readd_chan);
}
} SMARTLIST_FOREACH_END(readd_chan);
```
## Patch
`defects/tor/patch/tor-0003-kist-readd-heap-idx-o1.patch`
## Unit Test
`defects/tor/unit/TorKistSchedulerTest.java`

View file

@ -0,0 +1,20 @@
# valkey-0001: SINTER/SDIFF on listpack sets — O(N×M) quadratic membership
**Target:** valkey-io/valkey
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/t_set.c` (`sinterGenericCommand`)
**Status:** PATCHED
## Description
Identical defect to redis-0001. Valkey forked Redis and carries the same
`sinterGenericCommand` / `sunionDiffGenericCommand` code paths with the same
O(N×M) listpack membership defect.
See redis-0001 for full analysis.
## Fix
See patch: `defects/valkey/patch/0001-sinter-listpack-promote-to-htset.patch`
(same logic as redis-0001 patch, applied to valkey source tree).

View file

@ -0,0 +1,20 @@
# valkey-0002: getUpcomingChannelList O(S×C²) quadratic channel membership
**Target:** valkey-io/valkey
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/acl.c` (`getUpcomingChannelList`)
**Status:** PATCHED
## Description
Identical defect to redis-0002. Valkey forked Redis and carries the same
`getUpcomingChannelList` implementation using `listSearchKey` over the
`upcoming` linked list.
See redis-0002 for full analysis.
## Fix
See patch: `defects/valkey/patch/0002-acl-upcoming-channels-dict.patch`
(same logic as redis-0002 patch, applied to valkey source tree).

View file

@ -0,0 +1,59 @@
# varnish-0001: BAN_CheckObject linear ban list walk per cache hit
**Target:** varnish-cache
**File:** `bin/varnishd/cache/cache_ban.c`
**Function:** `BAN_CheckObject`
**Lines:** 688–693
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
On every cache hit, Varnish calls `BAN_CheckObject` to determine whether the
cached object is covered by a newer ban. The function walks the global ban list
from `ban_start` to the object's own ban pointer:
```c
for (b = b0; b != bn; b = VTAILQ_NEXT(b, list)) {
CHECK_OBJ_NOTNULL(b, BAN_MAGIC);
if (b->flags & BANS_FLAG_COMPLETED)
continue;
if (ban_evaluate(wrk, b->spec, oc, req->http, &tests))
break;
}
```
`ban_evaluate` itself evaluates the ban expression (URL/header regex or field
comparison) against the object. The list is kept in insertion order (newest
first). An object created at ban-list position K must check K pending bans on
every hit until the lurker catches up.
With B pending bans and H cache hits/second, total `ban_evaluate` calls =
O(B × H). A sustained ban workload (e.g., frequent content invalidation) where
the lurker falls behind creates a compounding penalty.
## Impact
- Workload: 1 000 pending bans × 10 000 cache hits/second = 10 000 000
ban_evaluate calls/second before lurker catches up.
- `ban_evaluate` walks the binary ban-spec and may execute regex per call.
- The per-request path holds `oc->objhead->mtx` for the duration of the loop.
## Fix
Two-stage approach:
1. The lurker already attempts to pre-test objects and move their `oc->ban`
pointer forward. Tuning `ban_lurker_batch` and `ban_lurker_sleep` downward
reduces the backlog.
2. Structural fix: index bans by the object fields they test (URL prefix,
specific header name). A trie keyed on URL or a per-field hash map lets
`BAN_CheckObject` skip non-matching bans in O(log B) or O(1) instead of
O(B).
## Patch
See `defects/varnish/patch/varnish-0001.patch`
## Unit Test
See `defects/varnish/unit/VarnishBanCheckAlgorithmTest.java`

View file

@ -0,0 +1,77 @@
# vlc-0001 — module_find() Linear Scan Per Module Existence Check (CWE-407)
**Status:** PATCHED
**Severity:** MEDIUM
**Target:** VLC `src/modules/modules.c`
**Function:** `module_find()`, `module_exists()`
## Defect
`module_find()` allocates and traverses the complete flat module list on every
call. `module_list_get()` (bank.c:829) walks all `vlc_plugins` linked-list to
build a `malloc`'d `module_t**` array, which is then scanned sequentially for a
name match:
```c
module_t *module_find(const char *name)
{
size_t count;
module_t **list = module_list_get(&count); // malloc + O(N) build
for (size_t i = 0; i < count; i++) // O(N) scan
if (!strcmp(module->pp_shortcuts[0], name))
...
}
```
`module_exists()` (vlc_modules.h:136) is just `module_find(name) != NULL`.
### Hot Call Sites
**`src/audio_output/output.c`** — calls `module_exists()` **5 times in
sequence** inside `aout_New()`, triggered once per audio output creation:
```
module_exists("goom") // O(N)
module_exists("projectm") // O(N)
module_exists("vsxu") // O(N)
module_exists("glspectrum") // O(N)
module_exists("equalizer") // O(N)
```
Plus one more at line 600: `module_exists("spatialaudio")`. That is 6 × O(N)
linear mallocs+scans, each allocating and freeing a full module pointer array.
**`src/preparser/internal.c:738`** — called inside a `for` loop over all
supported thumbnail formats, checking `module_exists(formats[i].module)`.
This is O(formats × modules) = O(N²) when the module list is large.
**`lib/media_player.c:721,732`** — two more calls at player creation.
## Root Cause
`module_find` uses no index. The capability-indexed lookup (`module_list_cap`,
bank.c:853) correctly uses a `tsearch` balanced BST, but name-based lookup
bypasses it entirely. A `g_hash_table` / `uthash` keyed on shortcut name
would reduce each call to O(1).
## Fix
Add a `name → module_t*` hash table populated during `vlc_bank_Load` / module
registration. `module_find()` replaces the scan with a `tfind` or hash
lookup. The `module_list_get` alloc is eliminated entirely for existence
checks.
Patch: `defects/vlc/patch/vlc-0001.patch`
## Complexity
| | Before | After |
|---|---|---|
| `module_find` | O(N) + malloc | O(1) |
| `module_exists` (×6 in aout) | 6 × O(N) + 6 malloc | 6 × O(1) |
| preparser format loop | O(formats × N) | O(formats) |
## Benchmark
See `defects/vlc/unit/VlcModuleFindTest.java` — at N=500 modules,
6 sequential scans vs 6 hash lookups; scan executes >200× more comparisons.

View file

@ -0,0 +1,15 @@
# wireguard-tools-0001: CLEAN
**Status:** CLEAN — no CWE-407 defect confirmed
## Analysis
`setconf.c` merges file and runtime peer lists via `qsort` + single linear pass — O(n log n),
not O(n²).
`show.c` iterates peers once per display field; no nested peer lookup.
`config.c` peer parsing is purely sequential; no membership test inside a loop.
WireGuard tools CLI processes peer counts in the hundreds at most (kernel limit ~1M but
CLI is not on any hot TLS/handshake path). No actionable quadratic pattern found.

View file

@ -0,0 +1,47 @@
# zookeeper-0001: PrepRequestProcessor ACL dedup — ArrayList.contains() O(n²)
**Target:** Apache ZooKeeper
**File:** `zookeeper-server/src/main/java/org/apache/zookeeper/server/PrepRequestProcessor.java`
**CWE:** CWE-407 — Inefficient Algorithmic Complexity
**Severity:** HIGH
**Status:** PATCHED
## Description
`PrepRequestProcessor.removeDuplicates(List<ACL>)` deduplicates ACL entries using
`retval.contains(acl)` where `retval` is an `ArrayList<ACL>`. For n ACL entries this
is O(n²). The method is called via `fixupACL()` on **every** `create` and `setACL`
request — it sits in the hot request-processing path.
```java
// PrepRequestProcessor.java line 943-957
private static List<ACL> removeDuplicates(final List<ACL> acls) {
// This would be done better with a Set but ACL hashcode/equals do not
// allow for null values
final ArrayList<ACL> retval = new ArrayList<>(acls.size());
for (final ACL acl : acls) {
if (!retval.contains(acl)) { // O(n) per iteration → O(n²) total
retval.add(acl);
}
}
return retval;
}
```
The comment acknowledges the defect but dismisses it on the grounds that `ACL`'s
`hashCode`/`equals` "do not allow for null values." In practice `ACL` fields are
`Id` objects whose own `hashCode` never returns null — the concern is unfounded.
## Fix
Replace the ArrayList accumulator with a `LinkedHashSet<ACL>` (preserves order, O(1)
lookup) and return `new ArrayList<>(set)`. Alternatively, add a null-safe comparator
and use a `TreeSet`. Either eliminates the O(n²) behaviour.
## Patch
`defects/zookeeper/patch/zookeeper-0001.patch`
## Unit Test
`defects/zookeeper/unit/ZooKeeperAclDedupTest.java`