From ec186e286cbcf1e94a5a9e16dc2026af9bcbac47 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 15:16:54 -0400 Subject: [PATCH] =?UTF-8?q?element-web-0003/0004=20+=20conduit-0001:=20CWE?= =?UTF-8?q?-407=20deep=20scan=20=E2=80=94=203=20new=20defects,=206/6=20PAS?= =?UTF-8?q?S?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...conduit-0001-earliest-events-hashset.patch | 21 ++ defects/conduit/unit/ConduitTest.java | 128 +++++++++++ ...element-web-0003-canonical-alias-set.patch | 17 ++ .../element-web-0004-arraydiff-set.patch | 26 +++ defects/element-web/unit/ElementWebTest.java | 210 +++++++++++++++++- 5 files changed, 396 insertions(+), 6 deletions(-) create mode 100644 defects/conduit/patch/conduit-0001-earliest-events-hashset.patch create mode 100644 defects/conduit/unit/ConduitTest.java create mode 100644 defects/element-web/patch/element-web-0003-canonical-alias-set.patch create mode 100644 defects/element-web/patch/element-web-0004-arraydiff-set.patch diff --git a/defects/conduit/patch/conduit-0001-earliest-events-hashset.patch b/defects/conduit/patch/conduit-0001-earliest-events-hashset.patch new file mode 100644 index 000000000..c5eabe444 --- /dev/null +++ b/defects/conduit/patch/conduit-0001-earliest-events-hashset.patch @@ -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; + } diff --git a/defects/conduit/unit/ConduitTest.java b/defects/conduit/unit/ConduitTest.java new file mode 100644 index 000000000..e1bf1da38 --- /dev/null +++ b/defects/conduit/unit/ConduitTest.java @@ -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 latestEvents, List earliestEvents, + Map> prevEventsMap, int limit) { + List queued = new ArrayList<>(latestEvents); + List 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 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 latestEvents, List earliestEvents, + Map> prevEventsMap, int limit) { + List queued = new ArrayList<>(latestEvents); + Set earliestSet = new HashSet<>(earliestEvents); // O(E) once + List 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 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 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> prevEventsMap = new HashMap<>(); + List 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 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 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"); + } +} diff --git a/defects/element-web/patch/element-web-0003-canonical-alias-set.patch b/defects/element-web/patch/element-web-0003-canonical-alias-set.patch new file mode 100644 index 000000000..943a48a8d --- /dev/null +++ b/defects/element-web/patch/element-web-0003-canonical-alias-set.patch @@ -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) { diff --git a/defects/element-web/patch/element-web-0004-arraydiff-set.patch b/defects/element-web/patch/element-web-0004-arraydiff-set.patch new file mode 100644 index 000000000..2462cf9b1 --- /dev/null +++ b/defects/element-web/patch/element-web-0004-arraydiff-set.patch @@ -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(a: T[], b: T[]): Diff { ++ // 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(a: T[], b: T[]): Diff { + * @returns The intersection of the arrays. + */ + export function arrayIntersection(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)); + } diff --git a/defects/element-web/unit/ElementWebTest.java b/defects/element-web/unit/ElementWebTest.java index e0b407c78..bd551cb6e 100644 --- a/defects/element-web/unit/ElementWebTest.java +++ b/defects/element-web/unit/ElementWebTest.java @@ -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 removedAliasesIncludes(List oldAlt, List newAlt) { + List removed = new ArrayList<>(); + for (String alias : oldAlt) { + if (!newAlt.contains(alias)) removed.add(alias); // O(N) per alias + } + return removed; + } + + static List addedAliasesIncludes(List oldAlt, List newAlt) { + List added = new ArrayList<>(); + for (String alias : newAlt) { + if (!oldAlt.contains(alias)) added.add(alias); + } + return added; + } + + static List removedAliasesSet(List oldAlt, List newAlt) { + Set newSet = new HashSet<>(newAlt); + List removed = new ArrayList<>(); + for (String alias : oldAlt) { + if (!newSet.contains(alias)) removed.add(alias); // O(1) per alias + } + return removed; + } + + static List addedAliasesSet(List oldAlt, List newAlt) { + Set oldSet = new HashSet<>(oldAlt); + List 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 oldAlt = new ArrayList<>(); + List 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 removedArr = removedAliasesIncludes(oldAlt, newAlt); + List removedSet = removedAliasesSet(oldAlt, newAlt); + Collections.sort(removedArr); + Collections.sort(removedSet); + assert removedArr.equals(removedSet) : "removed aliases must agree"; + + List addedArr = addedAliasesIncludes(oldAlt, newAlt); + List 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> arrayDiffIncludes(List a, List b) { + List added = new ArrayList<>(); + for (String i : b) { + if (!a.contains(i)) added.add(i); // O(A) per element + } + List removed = new ArrayList<>(); + for (String i : a) { + if (!b.contains(i)) removed.add(i); // O(B) per element + } + Map> result = new HashMap<>(); + result.put("added", added); + result.put("removed", removed); + return result; + } + + static Map> arrayDiffSet(List a, List b) { + Set setA = new HashSet<>(a); + Set setB = new HashSet<>(b); + List added = new ArrayList<>(); + for (String i : b) { + if (!setA.contains(i)) added.add(i); // O(1) per element + } + List removed = new ArrayList<>(); + for (String i : a) { + if (!setB.contains(i)) removed.add(i); + } + Map> result = new HashMap<>(); + result.put("added", added); + result.put("removed", removed); + return result; + } + + static List arrayIntersectionIncludes(List a, List b) { + List result = new ArrayList<>(); + for (String i : a) { + if (b.contains(i)) result.add(i); // O(B) per element + } + return result; + } + + static List arrayIntersectionSet(List a, List b) { + Set setB = new HashSet<>(b); + List 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 a = new ArrayList<>(); + List b = new ArrayList<>(); + for (int i = 0; i < N; i++) { + a.add("room_" + i); + b.add("room_" + (i + N / 2)); + } + + // correctness: arrayDiff + Map> d1 = arrayDiffIncludes(a, b); + Map> d2 = arrayDiffSet(a, b); + List added1 = new ArrayList<>(d1.get("added")); Collections.sort(added1); + List added2 = new ArrayList<>(d2.get("added")); Collections.sort(added2); + assert added1.equals(added2) : "arrayDiff added must agree"; + List rem1 = new ArrayList<>(d1.get("removed")); Collections.sort(rem1); + List rem2 = new ArrayList<>(d2.get("removed")); Collections.sort(rem2); + assert rem1.equals(rem2) : "arrayDiff removed must agree"; + + // correctness: arrayIntersection + List i1 = arrayIntersectionIncludes(a, b); + List 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"); } }