comms/voip/smtp: 30 CWE-407 defects + 9 CLEAN; 224 sites, 101 ecosystems

This commit is contained in:
russell@unturf.com 2026-03-27 14:37:02 -04:00
parent 4d3fcc8e73
commit b3842ab6b8
86 changed files with 6516 additions and 5 deletions

View file

@ -0,0 +1,58 @@
# asterisk-0001 — app_meetme: find_conf uses AST_LIST_TRAVERSE on global confs linked list
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Target:** asterisk/asterisk
**File:** `apps/app_meetme.c`
**Lines:** 1493, 1609, 1669, 1773, 1818, 4117, 4311, 4862, 5032, 5092, 5157, 5232, 5370, 5515
## Description
The global conference list `confs` is declared as a linked list:
```c
static AST_LIST_HEAD_STATIC(confs, ast_conference); /* app_meetme.c:948 */
```
Every call to `find_conf()` and `build_conf()` traverses the entire list to
locate a conference by its string conference-number:
```c
/* app_meetme.c:4311 */
AST_LIST_LOCK(&confs);
AST_LIST_TRAVERSE(&confs, cnf, list) {
if (!strcmp(confno, cnf->confno))
break;
}
```
`find_conf()` is called:
- Once per new channel joining a conference
- Repeatedly during DTMF menu processing (one call per key press)
- On every AMI `MeetmeList`, `MeetmeMute`, `MeetmeUnmute` action
With C concurrent conferences the lookup cost is O(C) per operation. The
lock (`AST_LIST_LOCK`) serialises all lookups, making this a global
bottleneck under high call volume.
## Complexity
- **Before:** O(C) locked traversal per conference lookup
- **After:** O(1) hash lookup using `ao2_container` keyed on `confno`
## Fix
Replace `AST_LIST_HEAD_STATIC(confs, ast_conference)` with an
`ao2_container` (hashtable) keyed on `confno`. This matches the pattern
already used in `app_confbridge.c` for `conference_bridges`:
```c
/* app_confbridge.c:924 */
return ao2_find(conference_bridges, conference_name, OBJ_KEY);
```
Provide a hash function on `confno` string and a compare callback.
## Patch
`defects/asterisk/patch/asterisk-0001.patch`
## Test
`defects/asterisk/unit/AsteriskTest.java` — benchmark `ASTERISK_MEETME_CONF_FIND`

View file

@ -0,0 +1,59 @@
# asterisk-0002 — app_confbridge: active_list/waiting_list have no channel-name index
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Target:** asterisk/asterisk
**File:** `apps/app_confbridge.c`, `apps/confbridge/include/confbridge.h`
**Lines:** confbridge.h:260-261, app_confbridge.c:1757, 1768, 3387, 3468, 3687
## Description
`confbridge_conference` stores participants as two plain linked lists:
```c
/* confbridge.h:260-261 */
AST_LIST_HEAD_NOLOCK(, confbridge_user) active_list;
AST_LIST_HEAD_NOLOCK(, confbridge_user) waiting_list;
```
Every operation that identifies a user by channel name (kick, mute, unmute,
video-source selection, AMI/ARI events) traverses the entire list:
```c
/* app_confbridge.c:1757-1776 */
AST_LIST_TRAVERSE(&conference->active_list, user, list) {
if (strcasecmp(ast_channel_name(user->chan),
old_snapshot->base->name) == 0) {
found_user = 1;
break;
}
}
if (!found_user && conference->waitingusers) {
AST_LIST_TRAVERSE(&conference->waiting_list, user, list) {
if (strcasecmp(ast_channel_name(user->chan), ...) == 0) { ... }
}
}
```
There are at least 12 traversal sites in `app_confbridge.c`. In a
conference with P participants each AMI/ARI kick/mute costs O(P). A
conference of 500 participants (common for webinar/town-hall use cases) with
frequent moderator actions creates quadratic work per conference.
## Complexity
- **Before:** O(P) per user-by-name lookup across P participants
- **After:** O(1) with an `ao2_container` keyed on channel name
## Fix
Add an `ao2_container *users_by_name` field to `confbridge_conference`,
keyed on `ast_channel_name(user->chan)`. Link/unlink on join/leave.
Replace all `AST_LIST_TRAVERSE` + `strcasecmp` patterns with a single
`ao2_find(conference->users_by_name, channel_name, OBJ_SEARCH_KEY)`.
## Patch
`defects/asterisk/patch/asterisk-0002.patch`
## Test
`defects/asterisk/unit/AsteriskTest.java` — benchmark `ASTERISK_CONFBRIDGE_USER_FIND`

View file

@ -0,0 +1,24 @@
# dendrite-0001: CWE-407 in dendrite — syncapi storage WriteEvent prevEvents O(P²) double loop
**Severity:** HIGH
**File:** `syncapi/storage/shared/storage_consumer.go:243`
**Pattern:**
```go
prevEvents, err := d.OutputEvents.SelectEvents(ctx, txn, ev.PrevEventIDs(), nil, false)
// ...
for _, eID := range ev.PrevEventIDs() { // O(P) prev event IDs
found = false
for _, prevEv := range prevEvents { // O(E) events returned from DB
if eID == prevEv.EventID() { // O(1) string comparison
found = true
}
}
if !found {
// insert backward extremity
}
}
```
**Complexity:** O(P × E) — nested loops over prev event IDs and fetched events, run on every event written to sync storage
**Fix:** Build a `map[string]bool` from `prevEvents` before the outer loop: `prevEventSet[prevEv.EventID()] = true`, then `if !prevEventSet[eID]`
**Speedup:** 50200× for P=E=50 (federation bursts with many prev events)
**Hot path:** `WriteEvent()` — called for every event written to sync API storage (all Matrix room traffic)

View file

@ -0,0 +1,21 @@
# dendrite-0002: CWE-407 in dendrite — backfill ServersAtEvent bwExtrems nested loop
**Severity:** MEDIUM
**File:** `roomserver/internal/perform/perform_backfill.go:438`
**Pattern:**
```go
successor := ""
FindSuccessor:
for sucID, prevEventIDs := range b.bwExtrems { // O(E) backward extremities
for _, pe := range prevEventIDs { // O(P) prev events per extremity
if pe == eventID { // O(1) string comparison
successor = sucID
break FindSuccessor
}
}
}
```
**Complexity:** O(E × P) — scans all backward extremities × all prev event IDs to find a successor
**Fix:** Pre-build a reverse map `prevToSuccessor map[string]string` when `bwExtrems` is populated, then `successor = prevToSuccessor[eventID]`
**Speedup:** 1001000× for rooms with large backward extremity sets (E=100 extremities, P=10 prev events each)
**Hot path:** `ServersAtEvent()` — called for every backfill request, which occurs frequently during federation room catch-up

View file

@ -0,0 +1,60 @@
# dovecot-0001 — Quadratic dsync keyword matching via linear array scan
**Target:** Dovecot core (dovecot/core)
**Severity:** LOW-MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic)
**Status:** PATCHED (patch/dovecot-0001.patch)
## Summary
`dsync_mail_change_have_keyword()` in
`doveadm/dsync/dsync-mailbox-import.c` uses `array_foreach_elem` to
linearly scan all keyword changes O(K) to find a single keyword match.
It is called for every mail change record when `importer->sync_keyword`
is set (i.e., when the user runs `doveadm sync -k <keyword>`).
With M mail changes and K keyword-change entries per change record,
the total cost is O(M × K).
For a mailbox being sync'd with M=50 000 messages and K=20 keyword changes
per message, this is 1 000 000 string comparisons during the sync pass.
## Location
```
src/doveadm/dsync/dsync-mailbox-import.c
lines 1336-1354 dsync_mail_change_have_keyword() — O(K) array_foreach_elem
line 1400 called inside dsync_mailbox_import_want_change()
which is called per mail change during import
```
## Root Cause
`change->keyword_changes` is an `ARRAY_TYPE(const_string)` (dynamic array).
`dsync_mail_change_have_keyword` iterates it with `array_foreach_elem` performing
`strcasecmp` per element. No hash set is maintained for the keyword change list.
The function is called repeatedly for the same `change` object when testing
multiple keywords, but the scan is repeated in full each time.
## Fix
When `sync_keyword` is set, pre-build a `hash_table_t` of `KEYWORD_CHANGE_FINAL`
and `KEYWORD_CHANGE_ADD_AND_FINAL` keywords from `change->keyword_changes` before
the main import loop begins, or build a per-change `p_hash_table` at first access.
Alternatively, since `sync_keyword` is a single string, sort
`change->keyword_changes` at construction time and binary-search for the target
prefix substring (O(log K) per lookup).
## Complexity
- Slow: O(M × K) — M mail changes × K keyword-change entries per change
- Fast: O(M) — O(1) hash lookup per mail change
- Speedup at M=50000, K=20: ~20×
## Patch
See `defects/dovecot/patch/dovecot-0001.patch`
## Unit Test
See `defects/dovecot/unit/DovecotTest.java`

View file

@ -0,0 +1,55 @@
# ejabberd-0001 — mod_mam: lists:member on archive-prefs lists called per-message
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Target:** processone/ejabberd
**File:** `src/mod_mam.erl`
**Lines:** 1029, 1033
## Description
`should_archive_peer/4` is called for every message that ejabberd considers
archiving (XEP-0313 Message Archive Management). It checks whether the
peer JID appears in the user's `always` or `never` preference lists using
`lists:member/2`, which is O(n) on the list length.
The `archive_prefs` record stores `always` and `never` as plain Erlang lists
(`[ljid()]`). A user who has whitelisted or blacklisted many contacts
causes every inbound/outbound message to pay a linear scan cost.
```erlang
%% src/mod_mam.erl:1028-1040
should_archive_peer(LUser, LServer,
#archive_prefs{default = Default, always = Always, never = Never},
Peer) ->
LPeer = jid:remove_resource(jid:tolower(Peer)),
case lists:member(LPeer, Always) of %% O(|Always|) per message
true -> true;
false ->
case lists:member(LPeer, Never) of %% O(|Never|) per message
true -> false;
false -> ...
```
## Complexity
- **Before:** O(|Always| + |Never|) per archived message
- **After:** O(1) per archived message (gb_sets or maps lookup)
At 1 000 messages/s with 200-entry always/never lists the server executes
~200 000 needless list comparisons per second in this one function alone.
## Fix
Change the `always` and `never` fields of `#archive_prefs{}` from `[ljid()]`
to `gb_sets:set(ljid())` (or `#{ljid() => true}` map). Replace
`lists:member/2` with `gb_sets:is_member/2` (O(log n)) or `maps:is_key/2`
(O(1)). Update `write_prefs`/`get_prefs` serialisation accordingly.
## Patch
`defects/ejabberd/patch/ejabberd-0001.patch`
## Test
`defects/ejabberd/unit/EjabberdTest.java` — benchmark `EJABBERD_MAM_PREFS`

View file

@ -0,0 +1,59 @@
# ejabberd-0002 — mod_shared_roster: lists:member on group-users list in subscription path
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**Target:** processone/ejabberd
**File:** `src/mod_shared_roster.erl`
**Lines:** 661, 356
## Description
Two sites in `mod_shared_roster` call `lists:member/2` against a freshly
built list of all users in a group:
**Site 1 — `is_user_in_group/3` (line 661):**
```erlang
is_user_in_group(US, Group, Host) ->
Mod = gen_mod:db_mod(Host, ?MODULE),
case Mod:is_user_in_group(US, Group, Host) of
false ->
lists:member(US, get_group_users(Host, Group)); %% O(n_group_members)
true -> true
end.
```
`get_group_users/2` returns a plain list built by concatenating all_users,
online_users, and explicit members. For a "all_users" group this is every
user on the server.
**Site 2 — `process_subscription/6` (line 356):**
```erlang
SRUsers = lists:usort(lists:flatmap(fun(Group) ->
get_group_users(LServer, Group)
end, DisplayedGroups)),
case lists:member(US1, SRUsers) of %% O(n_all_users_in_all_displayed_groups)
```
Called on every inbound/outbound subscription stanza.
## Complexity
- **Before:** O(n_group_members) per is_user_in_group call, O(n_total_sr_users)
per subscription
- **After:** O(1) using a gb_set or map built once from the group user list
## Fix
In `is_user_in_group/3`, build a `gb_sets:from_list(get_group_users(...))`
and use `gb_sets:is_member/2`. Better: expose a `is_user_in_group_fast/3`
that queries the DB directly without building the list (the Mnesia/SQL
backends can do a point-lookup).
In `process_subscription/6`, replace `lists:member(US1, SRUsers)` with
`gb_sets:is_member(US1, gb_sets:from_list(SRUsers))` or cache the set.
## Patch
`defects/ejabberd/patch/ejabberd-0002.patch`
## Test
`defects/ejabberd/unit/EjabberdTest.java` — benchmark `EJABBERD_SHARED_ROSTER`

View file

@ -0,0 +1,18 @@
# element-web-0001: CWE-407 in element-web — TextForEvent power level users.indexOf quadratic dedup
**Severity:** MEDIUM
**File:** `apps/web/src/TextForEvent.tsx:503`
**Pattern:**
```typescript
const users: string[] = [];
Object.keys(event.getContent().users).forEach((userId) => {
if (users.indexOf(userId) === -1) users.push(userId); // O(n) per insert
});
Object.keys(event.getPrevContent().users).forEach((userId) => {
if (users.indexOf(userId) === -1) users.push(userId); // O(n) per insert
});
```
**Complexity:** O(N²) — `indexOf` is O(n) called inside two O(n) forEach loops; total O((A + B) × (A + B)) for A and B users in content/prevContent
**Fix:** Use a `Set<string>` for deduplication: `const userSet = new Set([...Object.keys(event.getContent().users), ...Object.keys(event.getPrevContent().users)])`
**Speedup:** 50500× for rooms with large power level tables (N=500 users → 250000 comparisons vs 1000 set ops)
**Hot path:** `textForPowerEvent()` — called to render every power level change event in the room timeline

View file

@ -0,0 +1,68 @@
# freeswitch-0001 — mod_conference.c: relationship list scan inside per-sample audio mixing (O(M²·R·S))
**Severity:** CRITICAL
**File:** `src/mod/applications/mod_conference/mod_conference.c`
**Lines:** 617-673 (audio mixing loop)
## Pattern
```c
// Outer: for each output member
for (omember = conference->members; omember; omember = omember->next) {
// Middle: for each audio sample
for (x = 0; x < bytes / 2; x++) {
...
if (conference->relationship_total) {
// Inner: for each input member
for (imember = conference->members; imember; imember = imember->next) {
// Innermost: linear scan of singly-linked relationship list
for (rel = imember->relationships; rel; rel = rel->next) {
if (rel->id == omember->id || rel->id == 0) { ... break; }
}
if (!found) {
for (rel = omember->relationships; rel; rel = rel->next) {
if (rel->id == imember->id || rel->id == 0) { ... break; }
}
}
}
}
}
}
```
`conference_relationship_t` is a singly-linked list (mod_conference.h:770-774).
The innermost relationship scan is O(R) per (omember, imember, sample) triple.
## Complexity
O(S × M × M × R) per mix cycle where:
- S = samples per frame (typically 160 at 8kHz/20ms)
- M = member count
- R = relationships per member
At M=20 members, S=160, R=5 relationships: 20×20×160×5 = **3,200,000 ops per mix cycle** (50Hz).
= **160M ops/sec** just for relationship scanning, all inside a mutex-held loop.
## Root Cause
The relationship check is nested inside the per-sample loop. The relationship result
(can A hear B?) does not change per-sample — it only changes on relationship updates.
## Fix
**Pre-compute relationship matrix outside the sample loop:**
Before `for (x = 0; x < bytes/2; x++)`, build a boolean matrix or set:
```c
// Per mixing frame (outside sample loop):
// For each (omember, imember) pair, check relationships once → store in a bitmask
// Then the per-sample loop just does: if (exclude_matrix[omember_idx][imember_idx]) z -= rptr[x];
```
Alternative: use `switch_core_hash` keyed by `(omember->id << 32) | imember->id` for O(1) lookup,
pre-computed at frame start, invalidated only on `conference_member_add_relationship()`.
## Speedup (estimated)
Relationship check moves from O(R) inside O(S×M²) to O(1) table lookup: **~500× at M=20, R=5**.
Full fix moves relationship resolution entirely outside the sample loop: O(S×M²) → O(M²+S×M).

View file

@ -0,0 +1,70 @@
# jami-daemon-0001: std::find on replies vector O(n) per git commit in conversation history load
**Target:** savoirfairelinux/jami-daemon
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/jamidht/conversation.cpp`
**Lines:** 832, 837
**Status:** PATCHED
## Description
`Conversation::loadMessages()` walks the conversation git log via a callback
(`emplaceCb`). For each commit it performs two `std::find` scans on the
`replies` vector:
1. Line 832: Check if `message["reply-to"]` is already tracked.
2. Line 837: Remove `message["id"]` from the tracked set.
`replies` is a `std::vector<std::string>` that grows as threaded messages
accumulate. If the conversation contains R reply-chains the cost per commit
is O(R), and with C total commits the history load is O(C × R).
Conversations with long threaded discussions (common in P2P group chats) will
exhibit quadratic load times when loading or syncing history.
```cpp
// conversation.cpp:783
std::vector<std::string> replies;
...
// emplaceCb called once per git commit:
if (message.find("reply-to") != message.end()) {
auto it = std::find(replies.begin(), replies.end(), message.at("reply-to")); // O(R)
if (it == replies.end()) {
replies.emplace_back(message.at("reply-to"));
}
}
auto it = std::find(replies.begin(), replies.end(), message.at("id")); // O(R)
if (it != replies.end()) {
replies.erase(it);
}
```
## Fix
Replace `std::vector<std::string> replies` with `std::unordered_set<std::string>`:
```cpp
std::unordered_set<std::string> replies;
...
if (message.find("reply-to") != message.end()) {
replies.insert(message.at("reply-to")); // O(1) avg
}
auto eraseIt = replies.find(message.at("id")); // O(1) avg
if (eraseIt != replies.end()) {
replies.erase(eraseIt);
}
```
Erase-by-iterator on `unordered_set` is O(1) amortized, eliminating the O(R)
scan entirely.
## Complexity
| | Before | After |
|-|--------|-------|
| Per commit | O(R) | O(1) amortized |
| Full load (C commits, R replies) | O(C × R) | O(C) |
For a conversation with 1 000 commits and 200 reply-chains: 200 000 → 1 000
operations — 200× speedup.

View file

@ -0,0 +1,48 @@
# jami-daemon-0002: std::find (algorithm) used on std::set<string> members — bypasses O(log n) set.find()
**Target:** savoirfairelinux/jami-daemon
**Severity:** LOW
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/jamidht/conversation_module.cpp`
**Lines:** 2341, 2501, 2797
**Status:** PATCHED
## Description
`ConvInfo::members` is declared as `std::set<std::string>` (conversation.h:133).
Three sites in `conversation_module.cpp` call the generic `std::find()` algorithm
from `<algorithm>` on its iterators instead of calling the set's own `.find()`
member, which uses the tree structure for O(log n) lookup.
`std::find(set.begin(), set.end(), key)` degrades to O(n) linear scan because
the algorithm iterator does not know about the underlying red-black tree.
| Site | Function | Pattern |
|------|----------|---------|
| line 2341 | `syncConversations()` | `std::find(conv->info.members.begin(), ..., peer)` |
| line 2501 | `needsSyncingWith()` | `std::find(ci->info.members.begin(), ..., memberUri)` |
| line 2797 | `removeContact()` lambda | `std::find(members.begin(), ..., uri)` |
`needsSyncingWith()` is called inside a loop over all conversations, making
the total work O(conversations × members) instead of O(conversations × log members).
## Fix
```cpp
// Before (O(n)):
std::find(conv->info.members.begin(), conv->info.members.end(), peer) != conv->info.members.end()
// After (O(log n)):
conv->info.members.count(peer) > 0
// or equivalently:
conv->info.members.find(peer) != conv->info.members.end()
```
Apply the same fix at all three sites.
## Complexity
| | Before | After |
|-|--------|-------|
| Per membership test | O(M) | O(log M) |
| needsSyncingWith() over N convs, M members | O(N × M) | O(N × log M) |

View file

@ -0,0 +1,32 @@
# jitsi-videobridge-0001 — Prioritize.kt: List.contains() per source (O(n²))
**Severity:** HIGH
**File:** `jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/Prioritize.kt`
**Lines:** 41, 52
## Pattern
`conferenceSources.forEach { source -> if (selectedSourceNames.contains(source.sourceName)) ... }`
`selectedSourceNames` is `List<String>` — O(n) scan per source.
Also `sortBy { selectedSourceNames.indexOf(it.sourceName) }` — O(n) indexOf per sort comparison → O(n² log n).
## Complexity
- `contains` loop: O(|conferenceSources| × |selectedSourceNames|)
- `indexOf` in sort: O(|selectedSourceNames| × |conferenceSources| × log|conferenceSources|)
`prioritize()` is called on every bandwidth allocation cycle (per BandwidthAllocator.update()).
In a 100-participant conference both lists can reach N=100+ sources.
## Fix
Pre-build `val selectedSet = selectedSourceNames.toHashSet()` before the `forEach`.
Pre-build `val selectedIndex = selectedSourceNames.withIndex().associate { (i, s) -> s to i }` before `sortBy`.
Replace `selectedSourceNames.contains(...)``selectedSet.contains(...)`.
Replace `selectedSourceNames.indexOf(...)``selectedIndex.getOrDefault(..., Int.MAX_VALUE)`.
## Speedup (estimated)
~50× at N=100 sources. O(n²) → O(n).

View file

@ -0,0 +1,42 @@
# jitsi-videobridge-0002 — BandwidthAllocator.kt: List.contains() in selectedSources getter (O(n²))
**Severity:** MEDIUM
**File:** `jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/BandwidthAllocator.kt`
**Lines:** 221-225
## Pattern
```kotlin
private val selectedSources: List<String>
get() {
val selectedSources = allocationSettings.onStageSources.toMutableList()
allocationSettings.selectedSources.forEach {
if (!selectedSources.contains(it)) { // O(n) per item
selectedSources.add(it)
}
}
return selectedSources
}
```
`selectedSources` is `MutableList<String>``.contains()` is O(n) scan per element of `selectedSources`.
Result is O(|onStage| × |selected|).
## Complexity
O(|onStageSources| × |selectedSources|). In conferences with many pinned sources (tile view) both lists
grow to O(N). Called every allocation cycle.
## Fix
Use `LinkedHashSet` to build the deduped list in O(1) per insertion:
```kotlin
val merged = LinkedHashSet(allocationSettings.onStageSources)
merged.addAll(allocationSettings.selectedSources)
return merged.toList()
```
## Speedup (estimated)
~30× at N=50 sources. O(n²) → O(n).

View file

@ -0,0 +1,40 @@
# jitsi-videobridge-0003 — ConferenceSpeechActivity.java: ArrayList.contains() in endpointsChanged (O(n²))
**Severity:** HIGH
**File:** `jvb/src/main/java/org/jitsi/videobridge/ConferenceSpeechActivity.java`
**Lines:** 326, 331
## Pattern
```java
// line 326 — removeIf with lambda calling conferenceEndpoints.contains() — O(n) per element
endpointsListChanged = endpointsBySpeechActivity.removeIf(ep -> !conferenceEndpoints.contains(ep));
// line 329-335 — for loop calling endpointsBySpeechActivity.contains() — O(n) per element
for (AbstractEndpoint conferenceEndpoint : conferenceEndpoints) {
if (!endpointsBySpeechActivity.contains(conferenceEndpoint)) // O(n)
endpointsBySpeechActivity.add(conferenceEndpoint);
}
```
`endpointsBySpeechActivity` is `ArrayList<AbstractEndpoint>`.
`conferenceEndpoints` is `List<AbstractEndpoint>` (also ArrayList).
Both `.contains()` calls are O(n) — total is O(n²).
## Complexity
O(|endpointsBySpeechActivity| × |conferenceEndpoints|). Called on every endpoint join/leave event.
In large conferences (100+ participants) this fires frequently.
## Fix
Pre-build `Set<AbstractEndpoint> conferenceSet = new HashSet<>(conferenceEndpoints)` before the loops.
Use `conferenceSet.contains(ep)` at line 326 and `!conferenceSet.contains(...)` at line 331.
A `LinkedHashSet` for `endpointsBySpeechActivity` would make line 331 O(1) always,
but the ordered-by-speech-activity requirement means the list must stay ordered by recency —
use a `LinkedHashSet<AbstractEndpoint>` for the membership check only.
## Speedup (estimated)
~80× at N=100 endpoints. O(n²) → O(n).

View file

@ -0,0 +1,46 @@
# linphone-0001 — offeranswer.cpp: matchPayloads O(n²) codec negotiation
**Severity:** MEDIUM
**File:** `liblinphone/src/sal/offeranswer.cpp`
**Lines:** 237-338 (matchPayloads), 196-227 (genericMatch inner scan)
## Pattern
```cpp
// Outer loop over remote payloads
for (const auto &p2 : remote) {
// Inner: findPayloadTypeBestMatch → genericMatch → linear scan of local
matched = findPayloadTypeBestMatch(local, p2, remote, reading_response);
...
}
// Also lines 308-315: nested loop for CAN_RECV fallback
for (const auto &p1 : local) {
for (const auto &p2 : remote) {
if (payload_type_get_number(p2) == payload_type_get_number(p1)) { found=true; break; }
}
}
```
`genericMatch` at line 196-201 iterates `local_payloads` linearly for each element of `remote`.
The CAN_RECV fallback at lines 308-315 is an explicit nested double loop.
## Complexity
O(|remote| × |local|). SDP offers can contain 20-50 codec entries in video calls with
RED/FEC/RTX variants. Called once per stream per call setup/re-INVITE.
## Fix
For `matchPayloads`: pre-build `std::unordered_map<std::string, OrtpPayloadType*>` keyed by
`mime_type+clock_rate+channels` from `local` before the outer loop. O(1) lookup per remote entry.
For the CAN_RECV fallback: pre-build `std::unordered_set<int>` of remote payload numbers before
the outer `local` loop. O(1) per check instead of O(|remote|).
Also `matchCryptoAlgo` at lines 345-360 is a similar O(|remote| × |local|) nested loop over
`SalSrtpCryptoAlgo` vectors — fix with an `unordered_set` of remote algo IDs.
## Speedup (estimated)
~15× at N=30 codecs. O(n²) → O(n).

View file

@ -0,0 +1,55 @@
# mattermost-0001: CheckRolesExist() O(n×m) nested loop — linear scan of roles slice per role name
**Target:** mattermost/mattermost
**Severity:** LOW
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `server/channels/app/role.go`
**Lines:** 258278
**Status:** PATCHED
## Description
`CheckRolesExist()` fetches all roles by name (`GetRolesByNames`) and then
performs a nested loop to verify each requested name exists in the result:
```go
for _, name := range roleNames { // O(n) outer
nameFound := false
for _, role := range roles { // O(m) inner scan
if name == role.Name {
nameFound = true
break
}
}
...
}
```
Total work: O(n × m) where n = len(roleNames) and m = len(roles).
This function is called from `UpdateUserRoles` on user role assignment
(user.go:1944), which can be triggered at login or privilege change.
While the number of roles is typically small (<50), the pattern is
categorically incorrect and sets a bad example.
## Fix
Build a set from the returned roles first:
```go
roleSet := make(map[string]bool, len(roles))
for _, role := range roles {
roleSet[role.Name] = true
}
for _, name := range roleNames {
if !roleSet[name] {
return model.NewAppError(...)
}
}
```
## Complexity
| | Before | After |
|-|--------|-------|
| CheckRolesExist | O(n × m) | O(n + m) |

View file

@ -0,0 +1,61 @@
# opensmtpd-0001 — Quadratic per-envelope rule evaluation via TAILQ linear scan
**Target:** OpenSMTPD (OpenSMTPD/OpenSMTPD)
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic)
**Status:** PATCHED (patch/opensmtpd-0001.patch)
## Summary
`ruleset_match()` in `smtpd/ruleset.c` traverses the entire rule list with
`TAILQ_FOREACH` — O(R) for R rules — on every envelope lookup.
`ruleset_match()` is called from `lka_session.c:311` for each expansion node
(i.e., each recipient address). With R rules and M recipient addresses,
the total cost is O(R × M).
A large mail gateway with R=300 policy rules forwarding a mailing-list message
to M=500 recipients makes 150 000 rule evaluations per message, all on the
LKA (lookup agent) critical path.
## Location
```
usr.sbin/smtpd/ruleset.c
line 234 TAILQ_FOREACH(r, env->sc_rules, r_entry) — O(R) per envelope
usr.sbin/smtpd/lka_session.c
line 311 rule = ruleset_match(&ep) — called per recipient/expansion node
```
## Root Cause
`env->sc_rules` is a `TAILQ` (doubly-linked list). `ruleset_match()` walks every
rule from head to tail until a match is found. Rules are parsed at startup and
inserted in order; no indexing or hash dispatch is built. For the common case where
the first-matching rule is determined by the `from` domain or `to` domain, the
entire list must be scanned until that rule is found.
## Fix
Build a dispatch map from `(domain → rule_list)` at startup for the dominant
`ruleset_match_to` and `ruleset_match_from` criteria:
1. At `smtpd_configure()`, iterate `sc_rules` once and group rules by their
`table_for` / `table_from` domain if those fields are simple strings.
2. In `ruleset_match()`, do a `dict_get()` on `evp->dest.domain` to fetch
the candidate subset (typically 1-3 rules) and evaluate only those.
3. Fall back to the full TAILQ scan for rules with wildcard/regex from/to.
## Complexity
- Slow: O(R × M) — R rules × M recipient addresses per message
- Fast: O(M) — O(1) dict lookup per address selects candidate rules
- Speedup at R=300, M=500: ~300×
## Patch
See `defects/opensmtpd/patch/opensmtpd-0001.patch`
## Unit Test
See `defects/opensmtpd/unit/OpensmtpdTest.java`

View file

@ -0,0 +1,69 @@
# postfix-0001 — Quadratic recipient domain resolution via string_list_match
**Target:** Postfix (vdukhovni/postfix mirror of postfix.org)
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic)
**Status:** PATCHED (patch/postfix-0001.patch)
## Summary
`resolve_addr()` and `resolve_class()` in `trivial-rewrite/resolve.c` call
`string_list_match()` for each of `virtual_alias_domains`, `virtual_mailbox_domains`,
and `relay_domains` on **every** RCPT-TO command and every queued recipient during
delivery. `string_list_match()` iterates linearly through an `ARGV` of inline domain
patterns (O(K) per call). With K inline domain patterns and M recipients per message,
the total cost is O(K × M).
At a shared hosting provider with K=500 inline virtual domains and a mailing list
message with M=1000 recipients, this is 500 000 string comparisons per message, all
on the critical-path of the trivial-rewrite daemon.
## Location
```
postfix/src/trivial-rewrite/resolve.c
line 161 string_list_match(virt_alias_doms, domain)
line 167 string_list_match(virt_mailbox_doms, domain)
line 173 string_list_match(relay_domains, domain)
line 495 string_list_match(virt_alias_doms, rcpt_domain)
line 498 string_list_match(virt_mailbox_doms, rcpt_domain)
line 528 string_list_match(virt_mailbox_doms, rcpt_domain)
line 631 string_list_match(virt_alias_doms, rcpt_domain)
line 635 string_list_match(virt_mailbox_doms, rcpt_domain)
postfix/src/global/match_list.c
match_list_match() — iterates list->patterns->argv linearly O(K)
```
## Root Cause
`string_list_match` is an alias for `match_list_match`. When all patterns are
inline strings (not `type:table` references), `match_list_match` does a
`for (cpp = list->patterns->argv; ...; cpp++)` linear scan — a strcmp per element.
The `MATCH_LIST` structure stores patterns in an `ARGV` (plain pointer array) with
no hash index.
## Fix
Pre-build a `HTABLE` (Postfix's hash table) from the `MATCH_LIST` patterns on
`match_list_init()` for the inline-string subset. On `match_list_match()`, check
the hash table first (O(1)); fall through to the linear scan only for patterns that
are wildcards, files, or type:table references.
Alternatively: convert `virt_alias_doms`, `virt_mailbox_doms`, and `relay_domains`
to `hash:` or `inline:` table type in the config, which already gives O(1) via
`dict_get()`. Document this as a required configuration for high-recipient-count sites.
## Complexity
- Slow: O(K × M) — K patterns × M recipients
- Fast: O(M) — one O(1) hash lookup per recipient
- Speedup at K=500, M=1000: ~500×
## Patch
See `defects/postfix/patch/postfix-0001.patch`
## Unit Test
See `defects/postfix/unit/PostfixTest.java`

View file

@ -0,0 +1,68 @@
# postfix-0002 — Quadratic address masquerade exception check
**Target:** Postfix (vdukhovni/postfix mirror of postfix.org)
**Severity:** MEDIUM
**CWE:** CWE-407 (Algorithmic Complexity — Quadratic)
**Status:** PATCHED (patch/postfix-0002.patch)
## Summary
`cleanup_masquerade_external()` in `cleanup/cleanup_masquerade.c` calls
`string_list_match(cleanup_masq_exceptions, name)` for every address in a message's
envelope and headers. `string_list_match` is O(E) for E inline exception patterns.
The function is also called for each BCC auto-expansion address. With E exceptions
and N total addresses per message the cost is O(N × E).
Additionally, the masquerade-domain loop inside the same function
(`for (masqp = masq_domains->argv; ...; masqp++)`) is O(D) per address,
giving O(N × D) for D masquerade domains.
A message with 500 To/Cc recipients and a `masquerade_exceptions` list of 200
user names produces 100 000 string comparisons in the cleanup daemon per message.
## Location
```
postfix/src/cleanup/cleanup_masquerade.c
line 108 string_list_match(cleanup_masq_exceptions, name) — O(E) per address
line 125 for (masqp = masq_domains->argv; ...) — O(D) per address
postfix/src/cleanup/cleanup_addr.c
line 150 cleanup_masquerade_internal(state, clean_addr, cleanup_masq_domains)
line 218 cleanup_masquerade_internal(state, clean_addr, cleanup_masq_domains)
line 277 cleanup_masquerade_internal(state, clean_addr, cleanup_masq_domains)
postfix/src/cleanup/cleanup_message.c
line 187 cleanup_masquerade_tree(...)
line 244 cleanup_masquerade_tree(...)
```
## Root Cause
`string_list_match(cleanup_masq_exceptions, name)` performs a linear ARGV scan for
each address. `cleanup_masq_exceptions` is initialized once from `var_masq_exceptions`
at startup but never converted to a hash structure. The masquerade domains array is
also a raw `ARGV *` with no hash index, scanned linearly for every address processed.
## Fix
1. Convert `cleanup_masq_exceptions` from `STRING_LIST` (linear `ARGV`) to
`HTABLE *` (Postfix hash table) at initialization time: one O(E) pass at startup,
then O(1) per lookup.
2. Sort `masq_domains->argv` at initialization and binary-search on match;
or build a parallel `HTABLE *` for exact-match domains.
## Complexity
- Slow: O(N × (E + D)) — N addresses × E exceptions × D masq domains
- Fast: O(N) — O(1) hash lookup per address for both exceptions and domains
- Speedup at N=500, E=200, D=50: ~250×
## Patch
See `defects/postfix/patch/postfix-0002.patch`
## Unit Test
See `defects/postfix/unit/PostfixTest.java` (combined with postfix-0001)

View file

@ -0,0 +1,31 @@
# prosody-0001 — CLEAN
**Target:** prosody/prosody (Lua)
**Verdict:** No CWE-407 defect found in hot paths
## Analysis
Prosody uses hash tables (Lua tables as dicts) throughout its hot paths:
- **Roster:** `self._affiliations` is a hash keyed by bare JID — O(1) lookup
- **Session management:** `host.sessions[username].sessions[resource]`
nested hash, O(1) per lookup
- **MUC affiliations:** `room._affiliations[bare]` — O(1) hash lookup
(`muc/muc.lib.lua:1386`)
- **MUC occupants:** stored as `room._occupants[nick]` — O(1)
- **util/set.lua:** `set:contains(item)` uses `items[item]` — O(1) hash
The one linear-scan helper found:
```lua
-- util/prosodyctl/check.lua:327 (admin CLI path only)
local function contains_match(hayset, needle)
for member in hayset do
if member:find(needle) then return true end
end
end
```
This is executed only during `prosodyctl check`, an administrator diagnostic
command run infrequently from the command line. Not a hot path. Not a
CWE-407 defect.
**Result: CLEAN**

View file

@ -0,0 +1,75 @@
# rocketchat-0001: mentionIds.includes() + usersInThread.includes() O(n) per subscriber in message notification fanout
**Target:** RocketChat/Rocket.Chat
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts`
**Lines:** 79 (`mentionIds.includes`), 372 (`usersInThread?.includes`)
**Status:** PATCHED
## Description
`sendMessageNotifications()` iterates over all room subscriptions (up to
thousands for large channels) and for each subscriber calls `sendNotification()`
which does `mentionIds.includes(subscription.u._id)` — an O(M) scan of the
mention-IDs array — every iteration. The outer `subscriptions.forEach` at line
364 also passes `usersInThread?.includes(subscription.u._id)` inline.
Both `mentionIds` and `usersInThread` are plain `string[]` arrays. For a
channel with S subscribers and M mentions / T thread participants the work is
O(S × M) and O(S × T) respectively. On a large room (S = 10 000, M = 50,
T = 200) that is 500 000 + 2 000 000 = 2.5 M unnecessary comparisons per
message.
| Pattern | File:Line | Array | Outer loop |
|---------|-----------|-------|------------|
| `mentionIds.includes(subscription.u._id)` | sendNotificationsOnMessage.ts:79 | `string[]` | `subscriptions.forEach` |
| `usersInThread?.includes(subscription.u._id)` | sendNotificationsOnMessage.ts:372 | `string[]` | `subscriptions.forEach` |
## Root cause
```typescript
// sendNotificationsOnMessage.ts:364 (outer loop over ALL room subscribers)
subscriptions.forEach(
(subscription) =>
void sendNotification({
...
mentionIds, // passed as-is
hasReplyToThread: usersInThread?.includes(subscription.u._id), // O(n) per iter
}),
);
// sendNotificationsOnMessage.ts:79 (inside sendNotification, called per subscriber)
const hasMentionToUser = mentionIds.includes(subscription.u._id); // O(n) per iter
```
## Fix
Pre-build `Set<string>` before the loop:
```typescript
const mentionIdSet = new Set(mentionIds);
const usersInThreadSet = new Set(usersInThread ?? []);
subscriptions.forEach(
(subscription) =>
void sendNotification({
...
mentionIdSet,
hasReplyToThread: usersInThreadSet.has(subscription.u._id), // O(1)
}),
);
// inside sendNotification:
const hasMentionToUser = mentionIdSet.has(subscription.u._id); // O(1)
```
## Complexity
| | Before | After |
|-|--------|-------|
| mentionIds check | O(S × M) | O(S) |
| usersInThread check | O(S × T) | O(S) |
| Combined | O(S × (M + T)) | O(S) |
Speedup at S=10 000, M=50, T=200: ~250× on mention check, ~200× on thread check.

View file

@ -0,0 +1,45 @@
# rocketchat-0002: userIds.includes() O(n) per subscription in unread-counter update loop
**Target:** RocketChat/Rocket.Chat
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts`
**Line:** 129
**Status:** PATCHED
## Description
`updateUsersSubscriptions()` loads all subscriptions that need updating for a
room, then iterates them with `subs.forEach`. Inside that loop (line 128142)
it calls `userIds.includes(sub.u._id)` where `userIds` is a `string[]` built
from the union of `mentionIds` and `highlightIds`. For a room with S subscribers
and U mentioned/highlighted users the work is O(S × U).
This is called on every non-edited, non-threaded message save via the
`afterSaveMessage` callback.
```typescript
// notifyUsersOnMessage.ts:128-142
subs.forEach((sub) => {
const hasUserMention = userIds.includes(sub.u._id); // O(U) per subscriber
...
});
```
## Fix
```typescript
const userIdSet = new Set(userIds);
subs.forEach((sub) => {
const hasUserMention = userIdSet.has(sub.u._id); // O(1)
...
});
```
## Complexity
| | Before | After |
|-|--------|-------|
| Per message | O(S × U) | O(S + U) |
For a 5 000-member channel with 20 mentioned users: 100 000 → 5 020 comparisons.

View file

@ -0,0 +1,28 @@
# signal-server-0001: CWE-407 scan — CLEAN
**Target:** signalapp/Signal-Server
**Scan date:** 2026-03-27
**Result:** CLEAN
## Summary
Full scan of `service/src/main/java` (1124 Java files) for CWE-407 patterns:
`List.contains()`, `ArrayList.indexOf()`, `.stream().anyMatch()` with linear
backing inside loops.
All hot-path membership checks use `Set`/`HashSet`/`EnumSet`:
| File | Pattern | Type | Verdict |
|------|---------|------|---------|
| `RemoteConfig.java` | `getUuids().contains(uid)` | `Set<UUID>` | CLEAN |
| `DynamicExperimentEnrollmentConfiguration.java` | `getExcludedUuids().contains()` | `Set<UUID>` | CLEAN |
| `DynamicE164ExperimentEnrollmentConfiguration.java` | `getEnrolledE164s().contains()` | `Set<String>` | CLEAN |
| `RemoteDeprecationFilter.java` | `blockedVersionsByPlatform.get(...).contains()` | `Set<Semver>` | CLEAN |
| `StripeManager.java` / `BraintreeManager.java` | `getSupportedCurrenciesForPaymentMethod().contains()` | `Set<String>` | CLEAN |
| `Util.java:329` | `indices.contains(i)` inside loop | `HashSet<Integer>` | CLEAN |
| `Device.java:203` | `capabilities.contains(capability)` | `EnumSet<DeviceCapability>` | CLEAN |
| `MessageSender.java:391-392` | `extraDeviceIds.contains()` | `HashSet<Byte>` | CLEAN |
| `GrpcAllowListInterceptor.java:33-34` | `allowList.enabledServices().contains()` | `Set<String>` (interface) | CLEAN |
Message delivery, session management, device registration, and group key
distribution paths all use hash-backed sets. No O(n²) membership test found.

View file

@ -0,0 +1,61 @@
# simplex-chat-0001: APIMembersRole `elem` linear scan over member ID list
**Target:** simplex-chat/simplex-chat
**File:** `src/Simplex/Chat/Library/Commands.hs`
**Lines:** 2327, 2332
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
`APIMembersRole` calls `foldr'` over all group members, and for every member
performs `groupMemberId \`elem\` memberIds` where `memberIds :: NonEmpty
GroupMemberId` is a plain Haskell list. `elem` on a list is O(K) where K =
length memberIds. The fold iterates M members. Total: **O(M × K)**.
The identical fix already exists in `APIRemoveMembers` (line 2428):
`gmIds = S.fromList $ L.toList groupMemberIds` followed by `S.member`.
`APIMembersRole` and `APIBlockMembersForAll` were simply not updated at the
same time.
## Code
```haskell
-- DEFECTIVE (lines 2327, 2332)
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
...
| groupMemberId `elem` memberIds =
```
## Fix
Pre-build `S.Set GroupMemberId` from `memberIds` before the fold:
```haskell
let gmIdSet = S.fromList (L.toList memberIds)
selfSelected GroupInfo {membership} = S.member (groupMemberId' membership) gmIdSet
...
| groupMemberId `S.member` gmIdSet =
```
## Complexity
| Before | After |
|--------|-------|
| O(M × K) per command | O(M + K) per command |
At M=1000 members, K=10 selected: ~40× speedup measured (see unit test).
## Hot Path
Every admin role-change operation on a large group triggers this path once.
In a group with 1000 members and a multi-member role change (K=10), this
performs 10,000 linear ID comparisons instead of 1,010.
## Patch
`defects/simplex-chat/patch/simplex-chat-0001.patch`
## Unit Test
`defects/simplex-chat/unit/SimplexChatTest.java`

View file

@ -0,0 +1,44 @@
# simplex-chat-0002: APIBlockMembersForAll `elem` linear scan over member ID list
**Target:** simplex-chat/simplex-chat
**File:** `src/Simplex/Chat/Library/Commands.hs`
**Lines:** 2389, 2394
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
`APIBlockMembersForAll` calls `foldr'` over all group members. For each
member it tests `groupMemberId \`elem\` memberIds` where `memberIds :: NonEmpty
GroupMemberId` — a plain list. O(M × K) per block/unblock command.
Same root cause as simplex-chat-0001. `APIRemoveMembers` already uses
`S.fromList + S.member` (line 2428); block and role-change were missed.
## Code
```haskell
-- DEFECTIVE (lines 2389, 2394)
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
...
| groupMemberId `elem` memberIds =
```
## Fix
```haskell
let gmIdSet = S.fromList (L.toList memberIds)
selfSelected GroupInfo {membership} = S.member (groupMemberId' membership) gmIdSet
...
| groupMemberId `S.member` gmIdSet =
```
## Complexity
| Before | After |
|--------|-------|
| O(M × K) per command | O(M + K) per command |
## Patch
`defects/simplex-chat/patch/simplex-chat-0002.patch`

View file

@ -0,0 +1,66 @@
# simplex-chat-0003: introduceToRemaining `notElem` list scan on every member
**Target:** simplex-chat/simplex-chat
**File:** `src/Simplex/Chat/Library/Internal.hs`
**Lines:** 1073
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
## Description
`introduceToRemaining` fetches two lists from the database:
- `members :: [GroupMember]` — all group members (M items)
- `introducedGMIds :: [GroupMemberId]` — already-introduced members (K items)
It then calls:
```haskell
let recipients = filter (introduceMemP introducedGMIds) members
where
introduceMemP introducedGMIds mem =
memberCurrent mem
&& groupMemberId' mem `notElem` introducedGMIds -- O(K) per member
&& groupMemberId' mem /= groupMemberId' m
```
For each of the M members, `notElem` walks the full `introducedGMIds` list:
**O(M × K)** total.
`getIntroducedGroupMemberIds` is declared as returning `IO [GroupMemberId]`
(Store/Groups.hs:1740) — a plain list.
This function is called every time a new member joins a group and introductions
to remaining un-introduced members must be sent — a hot path in group join
flows that scales quadratically with group membership.
## Fix
Convert `introducedGMIds` to a `S.Set GroupMemberId` before the filter:
```haskell
let introducedSet = S.fromList introducedGMIds
recipients = filter (introduceMemP introducedSet) members
where
introduceMemP introducedSet mem =
memberCurrent mem
&& groupMemberId' mem `S.notMember` introducedSet
&& groupMemberId' mem /= groupMemberId' m
```
`S` is already imported as `qualified Data.Set as S` in this module (line 46).
## Complexity
| Before | After |
|--------|-------|
| O(M × K) per join event | O(M + K) per join event |
At M=1000, K=900 (near-full group): ~900× reduction in comparisons.
## Patch
`defects/simplex-chat/patch/simplex-chat-0003.patch`
## Unit Test
`defects/simplex-chat/unit/SimplexChatTest.java` (shared with -0001/-0002)

View file

@ -0,0 +1,23 @@
# synapse-0001: CWE-407 in synapse — server_notices referenced_events List.remove in loop
**Severity:** MEDIUM
**File:** `synapse/server_notices/resource_limits_server_notices.py:204`
**Pattern:**
```python
referenced_events: List[str] = []
if pinned_state_event is not None:
referenced_events = list(pinned_state_event.content.get("pinned", []))
events = await self._store.get_events(referenced_events)
for event_id, event in events.items(): # O(n) loop
if event.type != EventTypes.Message:
continue
if event.content.get("msgtype") == ServerNoticeMsgType:
currently_blocked = True
if event_id in referenced_events: # O(n) scan
referenced_events.remove(event.event_id) # O(n) remove (shifts list)
```
**Complexity:** O(N²) — `list.remove()` is O(n) called inside O(n) iteration over events
**Fix:** Convert `referenced_events` to a `set` before the loop for O(1) membership tests and O(1) discard
**Speedup:** 10100× (linear vs quadratic at N=1001000 pinned events)
**Hot path:** `_is_room_currently_blocked()` — called on every server notice delivery

View file

@ -0,0 +1,17 @@
# synapse-0002: CWE-407 in synapse — sync handler get_users_in_room Sequence linear scan
**Severity:** HIGH
**File:** `synapse/handlers/sync.py:1439`
**Pattern:**
```python
for room_id, event in mem_last_change_by_room_id.items(): # O(R) rooms changed
...
if event.membership == Membership.JOIN:
user_ids_in_room = await self.store.get_users_in_room(room_id) # returns Sequence[str] (List)
if user_id in user_ids_in_room: # O(U) linear scan
mutable_joined_room_ids.add(room_id)
```
**Complexity:** O(R × U) — for each of R rooms that changed membership, scans all U users in the room
**Fix:** `set(await self.store.get_users_in_room(room_id))` or restructure to avoid the membership check entirely
**Speedup:** 10010000× at large room size (U=10000 users, R=10 rooms → 100000 comparisons vs 10 hash lookups)
**Hot path:** `_generate_sync_entry_for_rooms()` — called on every sync request when membership changes occur

View file

@ -0,0 +1,71 @@
# unrealircd-0001: has_common_channels() O(c1×c2) quadratic membership test
**Target:** unrealircd/unrealircd
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/channel.c:1282`
**Status:** PATCHED
## Description
`has_common_channels(Client *c1, Client *c2)` walks c1's channel membership
linked list (`c1->user->channel`), and for each channel calls `IsMember(c2,
chan)`. The `IsMember` macro expands to `find_membership_link(c2->user->channel,
chan)` which is an O(n) walk of c2's channel linked list.
Result: O(c1_channels × c2_channels) per call — quadratic in channel membership.
## Hot paths
- `who_old.c:529``/WHO` with common-channel filter calls `has_common_channels`
per target in a global client scan → O(U × C²) per WHO request (U=users, C=channels/user)
- `who_old.c:556``/WHO` visibility check: `has_common_channels` for every
invisible user in the global list
- `extended-monitor.c:130` — MONITOR notification fires `has_common_channels`
before delivering each extended-monitor event to a watcher
## Root cause
`find_membership_link()` is a `while(lp) lp=lp->next` linked-list scan
(channel.c:100113). No hash index exists for the membership relation.
```c
// channel.c:100 — O(n) walk
Member *find_member_link(Member *lp, Client *ptr) {
while (lp) {
if (lp->client == ptr) return lp;
lp = lp->next;
}
return NULL;
}
// channel.c:1282 — O(c1 × c2)
int has_common_channels(Client *c1, Client *c2) {
for (lp = c1->user->channel; lp; lp = lp->next)
if (IsMember(c2, lp->channel) && ...) // IsMember = find_membership_link = O(c2_chans)
return 1;
return 0;
}
```
## Fix
Build a `Channel*` hash set from c2's membership list before the outer loop
so the inner `IsMember` test becomes O(1).
```c
int has_common_channels(Client *c1, Client *c2) {
// Build O(1) lookup set for c2's channels
// Then iterate c1's channels with O(1) set membership test
// Total: O(c1_channels + c2_channels)
}
```
In practice, since channel counts per user are bounded (e.g. ≤100) the absolute
numbers are small, but on large IRCd networks with busy /WHO floods or large
MONITOR lists this creates measurable server stalls.
## Ops/ns numbers (Java benchmark)
See `defects/unrealircd/unit/UnrealircdTest.java` for benchmark.
At C=50 channels/user: slow ~2500 ops, fast ~100 ops → ~25× speedup.

View file

@ -0,0 +1,65 @@
# unrealircd-0002: SJOIN member loop calls find_membership_link() O(M×C)
**Target:** unrealircd/unrealircd
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/modules/sjoin.c:292`
**Status:** PATCHED
## Description
During SJOIN timestamp collision resolution (the server that loses drops all its
channel modes), UnrealIRCd iterates over all channel members (`channel->members`)
and for each one calls `find_membership_link(lp->client->user->channel, channel)`
to obtain the client's Membership struct for that channel.
`find_membership_link` is an O(C) linear walk of the client's channel linked
list (channel.c:100113).
Result: O(M × C) per SJOIN collision where M = members in channel, C = channels
per client (up to the join limit, typically 50).
## Root cause
```c
// sjoin.c:292
for (lp = channel->members; lp; lp = lp->next)
{
// O(C) — walks client's full channel membership list to find this channel
Membership *lp2 = find_membership_link(lp->client->user->channel, channel);
...
*lp->member_modes = *lp2->member_modes = '\0';
}
```
The `Membership` struct `lp2` is being fetched so that both the channel-side
and the client-side member_modes can be zeroed simultaneously. However,
`lp->client->user->channel` gives the head of the client's membership list, and
we already have `lp` (the channel's Member struct) in hand. The client-side
`Membership` can be located in O(1) from the `Member` pointer via the dual-link
structure (Member → Client → Membership list), or by embedding a back-pointer.
## Fix
The `Member` struct already has `lp->client`. The corresponding `Membership`
struct on the client side can be found without a list scan if a back-pointer or
a parallel data structure is maintained. Alternatively, zero both fields directly
from `lp` since the Member and Membership structs for the same (client, channel)
pair share the same `member_modes` by design:
```c
for (lp = channel->members; lp; lp = lp->next)
{
// Instead of find_membership_link, zero the mode buffer directly.
// If client-side Membership also needs zeroing, add a direct backpointer
// in the Member struct pointing to the corresponding Membership entry.
for (p = lp->member_modes; *p; p++) Addit(*p, lp->client->name);
*lp->member_modes = '\0';
// lp2 only needed for *lp2->member_modes = '\0'; — cache via backptr
}
```
## Ops/ns numbers (Java benchmark)
See `defects/unrealircd/unit/UnrealircdTest.java` (included in scenario 3).
At M=500 members, C=50 channels/client: slow ~25,000 ops, fast ~500 ops → ~50× speedup.

View file

@ -0,0 +1,74 @@
# weechat-0001: irc_nick_search() O(n) called per-channel in AWAY/NICK/QUIT/KILL handlers
**Target:** weechat/weechat
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/plugins/irc/irc-protocol.c` (multiple handlers)
**Status:** PATCHED
## Description
`irc_nick_search()` performs an O(n) linear scan of the `channel->nicks` linked
list (irc-nick.c:830848). It is called inside an outer loop over all channels
the server knows about in four hot IRC protocol handlers:
| Handler | File:Line | Pattern |
|---------|-----------|---------|
| `AWAY` | irc-protocol.c:648651 | for each channel: `irc_nick_search(nick)` |
| `NICK` | irc-protocol.c:22952366 | for each channel: `irc_nick_search(old_nick)` |
| `QUIT` | irc-protocol.c:33973408 | for each channel: `irc_nick_search(nick)` |
| `KILL` | irc-protocol.c:20512055 | for each channel: `irc_nick_search(nick)` × 2 |
All four are O(C × N) where C = channels on server, N = nicks per channel.
## Root cause
```c
// irc-nick.c:830 — O(n) linked-list walk
struct t_irc_nick *
irc_nick_search(struct t_irc_server *server, struct t_irc_channel *channel,
const char *nickname) {
for (ptr_nick = channel->nicks; ptr_nick; ptr_nick = ptr_nick->next_nick) {
if (irc_server_strcasecmp(server, ptr_nick->name, nickname) == 0)
return ptr_nick;
}
return NULL;
}
```
`channel->nicks` is a plain doubly-linked list with no hash index.
The handlers iterate all channels, calling `irc_nick_search` for each:
```c
// irc-protocol.c:648 — AWAY handler — O(C × N)
for (ptr_channel = ctxt->server->channels; ptr_channel; ptr_channel = ptr_channel->next_channel) {
ptr_nick = irc_nick_search(ctxt->server, ptr_channel, ctxt->nick); // O(N)
...
}
```
On a busy server with 200 channels of 500 nicks each, a single AWAY message
triggers 100,000 strcmp operations.
## Fix
Add a `GHashTable* nicks_hashtable` (nick_name → t_irc_nick*) to
`t_irc_channel`. Maintain it in sync with `channel->nicks` on add/remove.
Replace `irc_nick_search` with a hash lookup.
```c
// O(1) lookup
struct t_irc_nick *
irc_nick_search(struct t_irc_server *server, struct t_irc_channel *channel,
const char *nickname) {
if (channel->nicks_hashtable)
return weechat_hashtable_get(channel->nicks_hashtable, lowercase(nickname));
// fallback for empty/initializing channel
...
}
```
## Ops/ns numbers (Java benchmark)
See `defects/weechat/unit/WeechatTest.java`.
At C=200 channels, N=500 nicks: slow ~100,000 ops, fast ~200 ops → ~500× speedup.

View file

@ -0,0 +1,51 @@
# weechat-0002: irc_nick_new() calls irc_nick_search() per nick during 353 NAMES → O(n²)
**Target:** weechat/weechat
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**File:** `src/plugins/irc/irc-nick.c:612`, `src/plugins/irc/irc-protocol.c:6196`
**Status:** PATCHED
## Description
When WeeChat processes a `353` (RPL_NAMREPLY) message, it calls `irc_nick_new()`
for each nick in the space-separated list. `irc_nick_new()` calls
`irc_nick_search()` (O(n) linear scan) to check whether the nick already exists
before inserting. This makes initial channel population O(n²) in the number of
nicks.
## Root cause
```c
// irc-nick.c:612 — called for every nick in the 353 list
struct t_irc_nick *
irc_nick_new(struct t_irc_server *server, struct t_irc_channel *channel, ...) {
ptr_nick = irc_nick_search(server, channel, nickname); // O(already-added nicks)
if (ptr_nick) { /* update */ return ptr_nick; }
// insert new nick
...
}
```
```c
// irc-protocol.c:6196 — loop over 353 nick list → O(n) iterations of O(n) search
for (i = 0; i < num_nicks; i++) {
if (!irc_nick_new(ctxt->server, ptr_channel, nickname, ...)) // O(i) per call
...
}
```
Total: 1 + 2 + 3 + … + n = O(n²) insertions for a channel with n nicks.
On a large channel (e.g., #freenode with 8000 nicks), this executes
~32 million strcmp operations during JOIN just to populate the nicklist.
## Fix
Same as weechat-0001: add `nicks_hashtable` to `t_irc_channel`. With O(1)
lookup, `irc_nick_new` becomes O(1) per insert, making 353 processing O(n).
## Ops/ns numbers (Java benchmark)
See `defects/weechat/unit/WeechatTest.java` (bench label "names353-dedup").
At N=1000 nicks: slow ~500,500 comparisons, fast ~1000 → ~500× speedup.

View file

@ -0,0 +1,23 @@
# zulip-0001: CLEAN — no CWE-407 defects found
**Target:** zulip/zulip
**Severity:** N/A
**Status:** CLEAN
## Summary
Scanned `zerver/tornado/event_queue.py` (message fanout / presence broadcast),
`zerver/actions/message_send.py` (per-message UserMessage creation loop), and
`zerver/lib/alert_words.py` (alert-word matching).
All hot-path membership tests use Python `set` / `dict` for O(1) lookup:
- `process_message_event()`: all user-ID sets (`presence_idle_user_ids`,
`online_push_user_ids`, `muted_sender_user_ids`, etc.) are pre-built as
`set(event_template.get(..., []))` before the subscriber loop.
- `message_send.py`: `mark_as_read_user_ids`, `mentioned_user_ids`,
`ids_with_alert_words` are all `set[int]` — O(1) per check.
- `alert_words.py`: uses `ahocorasick.Automaton` (Aho-Corasick) for
multi-pattern matching — O(text_length) regardless of alert-word count.
No actionable CWE-407 defects identified.