element-web-0003/0004 + conduit-0001: CWE-407 deep scan — 3 new defects, 6/6 PASS

element-web-0003: TextForEvent.tsx textForCanonicalAliasEvent alt_aliases
  .filter(x => !arr.includes(x)) O(A*B) MEDIUM 19.5x — fix: Set.has()

element-web-0004: utils/arrays.ts arrayDiff + arrayIntersection
  .filter(i => !arr.includes(i)) O(A*B) MEDIUM 18x — fix: Set.has()
  Used by 15+ callsites: room-list, notifications, spaces, beacons, maps

conduit-0001: server_server.rs get_missing_events_route
  earliest_events.contains() (Vec) in growing BFS loop O(Q*E) MEDIUM-HIGH 7.4x
  Federation endpoint — remote server controls input size

matrix-rust-sdk: could not clone (GitHub HTTPS unreachable)
This commit is contained in:
russell@unturf.com 2026-03-30 15:16:54 -04:00
parent f35e47bab3
commit ec186e286c
5 changed files with 396 additions and 6 deletions

View file

@ -0,0 +1,21 @@
--- a/src/api/server_server.rs
+++ b/src/api/server_server.rs
@@ -1254,6 +1254,9 @@ pub async fn get_missing_events_route(
let mut queued_events = body.latest_events.clone();
let mut events = Vec::new();
+ // CWE-407 fix: convert earliest_events to HashSet for O(1) membership test.
+ // Old code: Vec::contains() is O(E) per check inside a growing BFS loop,
+ // yielding O(Q*E) total. New code: O(Q) total via HashSet.
+ let earliest_set: HashSet<_> = body.earliest_events.iter().cloned().collect();
let mut i = 0;
while i < queued_events.len() && events.len() < u64::from(body.limit) as usize {
@@ -1281,7 +1284,7 @@ pub async fn get_missing_events_route(
}
- if body.earliest_events.contains(&queued_events[i]) {
+ if earliest_set.contains(&queued_events[i]) {
i += 1;
continue;
}

View file

@ -0,0 +1,128 @@
import java.util.*;
/**
* CWE-407 unit tests for Conduit (Rust Matrix homeserver) defects.
*
* conduit-0001: server_server.rs get_missing_events_route()
* body.earliest_events.contains() (Vec) inside growing BFS while-loop
* O(Q*E) where Q = queued events, E = earliest events.
* Fix: convert earliest_events to HashSet for O(1) membership test.
* Severity: MEDIUM-HIGH (federation endpoint, remote server controls input)
*/
public class ConduitTest {
// --- conduit-0001: earliest_events Vec::contains in BFS loop ---
/**
* Simulates the defective path: for each queued event, do a linear scan
* of earliest_events (ArrayList.contains -> O(E)).
* As BFS expands queued_events, total work = O(Q * E).
*/
static int getMissingEventsVec(List<String> latestEvents, List<String> earliestEvents,
Map<String, List<String>> prevEventsMap, int limit) {
List<String> queued = new ArrayList<>(latestEvents);
List<String> result = new ArrayList<>();
int i = 0;
int ops = 0;
while (i < queued.size() && result.size() < limit) {
String eventId = queued.get(i);
// Linear scan of earliestEvents the defect
ops++;
if (earliestEvents.contains(eventId)) { // O(E) each time
i++;
continue;
}
result.add(eventId);
// Expand BFS: add prev_events
List<String> prevEvents = prevEventsMap.getOrDefault(eventId, Collections.emptyList());
queued.addAll(prevEvents);
i++;
}
return ops;
}
/**
* Simulates the fixed path: convert earliest_events to HashSet once,
* then O(1) membership test per queued event.
*/
static int getMissingEventsSet(List<String> latestEvents, List<String> earliestEvents,
Map<String, List<String>> prevEventsMap, int limit) {
List<String> queued = new ArrayList<>(latestEvents);
Set<String> earliestSet = new HashSet<>(earliestEvents); // O(E) once
List<String> result = new ArrayList<>();
int i = 0;
int ops = 0;
while (i < queued.size() && result.size() < limit) {
String eventId = queued.get(i);
ops++;
if (earliestSet.contains(eventId)) { // O(1) each time
i++;
continue;
}
result.add(eventId);
List<String> prevEvents = prevEventsMap.getOrDefault(eventId, Collections.emptyList());
queued.addAll(prevEvents);
i++;
}
return ops;
}
static void testConduit0001() throws Exception {
// Build a synthetic DAG: E earliest events, chain of events leading back to them
int E = 500; // earliest events count
int depth = 50; // BFS depth
int limit = 10000;
List<String> earliestEvents = new ArrayList<>();
for (int i = 0; i < E; i++) {
earliestEvents.add("$earliest_" + i);
}
// Build a DAG where each event has 2 prev_events, creating a growing BFS frontier
Map<String, List<String>> prevEventsMap = new HashMap<>();
List<String> latestEvents = new ArrayList<>();
latestEvents.add("$latest_0");
// Create chain: latest -> intermediate events -> eventually hits earliest
String current = "$latest_0";
for (int d = 0; d < depth; d++) {
List<String> prevs = new ArrayList<>();
prevs.add("$event_" + d + "_a");
prevs.add("$event_" + d + "_b");
prevEventsMap.put(current, prevs);
current = "$event_" + d + "_a";
}
// Terminal events point to earliest events
List<String> terminalPrevs = new ArrayList<>();
terminalPrevs.add(earliestEvents.get(0));
terminalPrevs.add(earliestEvents.get(E / 2));
prevEventsMap.put(current, terminalPrevs);
// Correctness: both paths produce the same op count pattern
// (they process same events, just with different contains() cost)
// Performance
long t0 = System.nanoTime();
for (int r = 0; r < 500; r++) {
getMissingEventsVec(latestEvents, earliestEvents, prevEventsMap, limit);
}
long tVec = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int r = 0; r < 500; r++) {
getMissingEventsSet(latestEvents, earliestEvents, prevEventsMap, limit);
}
long tSet = System.nanoTime() - t0;
double ratio = (double) tVec / tSet;
System.out.printf("conduit-0001: vec=%.3fs set=%.3fs ratio=%.1f× (E=%d, depth=%d)%n",
tVec / 1e9, tSet / 1e9, ratio, E, depth);
assert ratio > 2 : "Expected >2x speedup, got " + ratio;
System.out.println("PASS conduit-0001");
}
public static void main(String[] args) throws Exception {
testConduit0001();
System.out.println("ALL PASS");
}
}

View file

@ -0,0 +1,17 @@
--- a/apps/web/src/TextForEvent.tsx
+++ b/apps/web/src/TextForEvent.tsx
@@ -394,8 +394,11 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): (() => string) | null {
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
const newAlias = ev.getContent().alias;
const newAltAliases = ev.getContent().alt_aliases || [];
- const removedAltAliases = oldAltAliases.filter((alias: string) => !newAltAliases.includes(alias));
- const addedAltAliases = newAltAliases.filter((alias: string) => !oldAltAliases.includes(alias));
+ // CWE-407 fix: use Set for O(1) membership test; avoids O(A*B) from
+ // includes() inside filter() when computing alias diffs.
+ const newAltAliasSet = new Set(newAltAliases);
+ const oldAltAliasSet = new Set(oldAltAliases);
+ const removedAltAliases = oldAltAliases.filter((alias: string) => !newAltAliasSet.has(alias));
+ const addedAltAliases = newAltAliases.filter((alias: string) => !oldAltAliasSet.has(alias));
if (!removedAltAliases.length && !addedAltAliases.length) {
if (newAlias) {

View file

@ -0,0 +1,26 @@
--- a/apps/web/src/utils/arrays.ts
+++ b/apps/web/src/utils/arrays.ts
@@ -189,8 +189,12 @@ export function arrayHasOrderChange(a: any[], b: any[]): boolean {
export function arrayDiff<T>(a: T[], b: T[]): Diff<T> {
+ // CWE-407 fix: use Set for O(1) membership test instead of O(A*B)
+ // includes() inside filter(). Build Sets once, then filter.
+ const setA = new Set(a);
+ const setB = new Set(b);
return {
- added: b.filter((i) => !a.includes(i)),
- removed: a.filter((i) => !b.includes(i)),
+ added: b.filter((i) => !setA.has(i)),
+ removed: a.filter((i) => !setB.has(i)),
};
}
@@ -202,7 +206,10 @@ export function arrayDiff<T>(a: T[], b: T[]): Diff<T> {
* @returns The intersection of the arrays.
*/
export function arrayIntersection<T>(a: T[], b: T[]): T[] {
- return a.filter((i) => b.includes(i));
+ // CWE-407 fix: use Set for O(1) membership test instead of O(A*B)
+ // includes() inside filter().
+ const setB = new Set(b);
+ return a.filter((i) => setB.has(i));
}

View file

@ -4,12 +4,20 @@ import java.util.*;
* CWE-407 unit tests for element-web (Matrix client) defects.
*
* element-web-0001: TextForEvent.tsx textForPowerEvent()
* users array + indexOf for dedup O(U²)
* Fix: Set-based dedup O(U)
* users array + indexOf for dedup -> O(U^2)
* Fix: Set-based dedup -> O(U)
*
* element-web-0002: TextForEvent.tsx textForPinnedEvent()
* previouslyPinned.indexOf inside filter O(P×Q)
* Fix: Set.has() inside filter O(P+Q)
* previouslyPinned.indexOf inside filter -> O(P*Q)
* Fix: Set.has() inside filter -> O(P+Q)
*
* element-web-0003: TextForEvent.tsx textForCanonicalAliasEvent()
* oldAltAliases.filter(a => !newAltAliases.includes(a)) -> O(A*B)
* Fix: Set.has() inside filter -> O(A+B)
*
* element-web-0004: utils/arrays.ts arrayDiff() and arrayIntersection()
* b.filter(i => !a.includes(i)) -> O(A*B)
* Fix: new Set(a) then setA.has() -> O(A+B)
*/
public class ElementWebTest {
@ -61,7 +69,7 @@ public class ElementWebTest {
double ratio = (double) tArr / tSet;
System.out.printf("element-web-0001: array=%.3fs set=%.3fs ratio=%.1f×%n",
tArr / 1e9, tSet / 1e9, ratio);
assert ratio > 10 : "Expected >10× speedup, got " + ratio;
assert ratio > 10 : "Expected >10x speedup, got " + ratio;
System.out.println("PASS element-web-0001");
}
@ -112,13 +120,203 @@ public class ElementWebTest {
double ratio = (double) tIndexOf / tSet;
System.out.printf("element-web-0002: indexOf=%.3fs set=%.3fs ratio=%.1f×%n",
tIndexOf / 1e9, tSet / 1e9, ratio);
assert ratio > 5 : "Expected >5× speedup, got " + ratio;
assert ratio > 5 : "Expected >5x speedup, got " + ratio;
System.out.println("PASS element-web-0002");
}
// --- element-web-0003: canonical alias alt_aliases diff ---
static List<String> removedAliasesIncludes(List<String> oldAlt, List<String> newAlt) {
List<String> removed = new ArrayList<>();
for (String alias : oldAlt) {
if (!newAlt.contains(alias)) removed.add(alias); // O(N) per alias
}
return removed;
}
static List<String> addedAliasesIncludes(List<String> oldAlt, List<String> newAlt) {
List<String> added = new ArrayList<>();
for (String alias : newAlt) {
if (!oldAlt.contains(alias)) added.add(alias);
}
return added;
}
static List<String> removedAliasesSet(List<String> oldAlt, List<String> newAlt) {
Set<String> newSet = new HashSet<>(newAlt);
List<String> removed = new ArrayList<>();
for (String alias : oldAlt) {
if (!newSet.contains(alias)) removed.add(alias); // O(1) per alias
}
return removed;
}
static List<String> addedAliasesSet(List<String> oldAlt, List<String> newAlt) {
Set<String> oldSet = new HashSet<>(oldAlt);
List<String> added = new ArrayList<>();
for (String alias : newAlt) {
if (!oldSet.contains(alias)) added.add(alias);
}
return added;
}
static void testElementWeb0003() throws Exception {
int A = 1000;
List<String> oldAlt = new ArrayList<>();
List<String> newAlt = new ArrayList<>();
for (int i = 0; i < A; i++) {
oldAlt.add("#alias" + i + ":matrix.org");
newAlt.add("#alias" + (i + A / 2) + ":matrix.org");
}
// correctness
List<String> removedArr = removedAliasesIncludes(oldAlt, newAlt);
List<String> removedSet = removedAliasesSet(oldAlt, newAlt);
Collections.sort(removedArr);
Collections.sort(removedSet);
assert removedArr.equals(removedSet) : "removed aliases must agree";
List<String> addedArr = addedAliasesIncludes(oldAlt, newAlt);
List<String> addedSetR = addedAliasesSet(oldAlt, newAlt);
Collections.sort(addedArr);
Collections.sort(addedSetR);
assert addedArr.equals(addedSetR) : "added aliases must agree";
// performance
long t0 = System.nanoTime();
for (int r = 0; r < 200; r++) {
removedAliasesIncludes(oldAlt, newAlt);
addedAliasesIncludes(oldAlt, newAlt);
}
long tArr = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int r = 0; r < 200; r++) {
removedAliasesSet(oldAlt, newAlt);
addedAliasesSet(oldAlt, newAlt);
}
long tSet = System.nanoTime() - t0;
double ratio = (double) tArr / tSet;
System.out.printf("element-web-0003: includes=%.3fs set=%.3fs ratio=%.1f×%n",
tArr / 1e9, tSet / 1e9, ratio);
assert ratio > 5 : "Expected >5x speedup, got " + ratio;
System.out.println("PASS element-web-0003");
}
// --- element-web-0004: arrayDiff + arrayIntersection ---
static Map<String, List<String>> arrayDiffIncludes(List<String> a, List<String> b) {
List<String> added = new ArrayList<>();
for (String i : b) {
if (!a.contains(i)) added.add(i); // O(A) per element
}
List<String> removed = new ArrayList<>();
for (String i : a) {
if (!b.contains(i)) removed.add(i); // O(B) per element
}
Map<String, List<String>> result = new HashMap<>();
result.put("added", added);
result.put("removed", removed);
return result;
}
static Map<String, List<String>> arrayDiffSet(List<String> a, List<String> b) {
Set<String> setA = new HashSet<>(a);
Set<String> setB = new HashSet<>(b);
List<String> added = new ArrayList<>();
for (String i : b) {
if (!setA.contains(i)) added.add(i); // O(1) per element
}
List<String> removed = new ArrayList<>();
for (String i : a) {
if (!setB.contains(i)) removed.add(i);
}
Map<String, List<String>> result = new HashMap<>();
result.put("added", added);
result.put("removed", removed);
return result;
}
static List<String> arrayIntersectionIncludes(List<String> a, List<String> b) {
List<String> result = new ArrayList<>();
for (String i : a) {
if (b.contains(i)) result.add(i); // O(B) per element
}
return result;
}
static List<String> arrayIntersectionSet(List<String> a, List<String> b) {
Set<String> setB = new HashSet<>(b);
List<String> result = new ArrayList<>();
for (String i : a) {
if (setB.contains(i)) result.add(i); // O(1) per element
}
return result;
}
static void testElementWeb0004() throws Exception {
int N = 1000;
List<String> a = new ArrayList<>();
List<String> b = new ArrayList<>();
for (int i = 0; i < N; i++) {
a.add("room_" + i);
b.add("room_" + (i + N / 2));
}
// correctness: arrayDiff
Map<String, List<String>> d1 = arrayDiffIncludes(a, b);
Map<String, List<String>> d2 = arrayDiffSet(a, b);
List<String> added1 = new ArrayList<>(d1.get("added")); Collections.sort(added1);
List<String> added2 = new ArrayList<>(d2.get("added")); Collections.sort(added2);
assert added1.equals(added2) : "arrayDiff added must agree";
List<String> rem1 = new ArrayList<>(d1.get("removed")); Collections.sort(rem1);
List<String> rem2 = new ArrayList<>(d2.get("removed")); Collections.sort(rem2);
assert rem1.equals(rem2) : "arrayDiff removed must agree";
// correctness: arrayIntersection
List<String> i1 = arrayIntersectionIncludes(a, b);
List<String> i2 = arrayIntersectionSet(a, b);
Collections.sort(i1);
Collections.sort(i2);
assert i1.equals(i2) : "arrayIntersection must agree";
// performance: arrayDiff
long t0 = System.nanoTime();
for (int r = 0; r < 200; r++) arrayDiffIncludes(a, b);
long tInc = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int r = 0; r < 200; r++) arrayDiffSet(a, b);
long tSet = System.nanoTime() - t0;
double ratio = (double) tInc / tSet;
System.out.printf("element-web-0004-diff: includes=%.3fs set=%.3fs ratio=%.1f×%n",
tInc / 1e9, tSet / 1e9, ratio);
assert ratio > 5 : "Expected >5x speedup for arrayDiff, got " + ratio;
// performance: arrayIntersection
t0 = System.nanoTime();
for (int r = 0; r < 200; r++) arrayIntersectionIncludes(a, b);
tInc = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int r = 0; r < 200; r++) arrayIntersectionSet(a, b);
tSet = System.nanoTime() - t0;
double ratio2 = (double) tInc / tSet;
System.out.printf("element-web-0004-intersect: includes=%.3fs set=%.3fs ratio=%.1f×%n",
tInc / 1e9, tSet / 1e9, ratio2);
assert ratio2 > 5 : "Expected >5x speedup for arrayIntersection, got " + ratio2;
System.out.println("PASS element-web-0004");
}
public static void main(String[] args) throws Exception {
testElementWeb0001();
testElementWeb0002();
testElementWeb0003();
testElementWeb0004();
System.out.println("ALL PASS");
}
}