# UNDF: UNDF-2026-000000538 # SM-0004: Modules.cpp ContainsElement(exportedNames) O(N) linear scan in GetExportedNames **File:** `js/src/vm/Modules.cpp` **Lines:** 510–518 (`ContainsElement` for ExportNameVector), 631 (call site in inner loop) **Severity:** MEDIUM **CWE:** CWE-407 (Inefficient Algorithmic Complexity) ## Description `ModuleGetExportedNames` implements the ECMAScript `GetExportedNames` abstract operation (ES2023 §16.2.1.6.2). When resolving `export *` entries it accumulates all exported names from re-exported modules into `exportedNames`, a `GCVector` (i.e. a plain growable array): ```cpp static bool ContainsElement(const ExportNameVector& list, JSAtom* atom) { for (JSAtom* a : list) { // O(N) linear scan — CWE-407 if (a == atom) return true; } return false; } // Inside ModuleGetExportedNames(): for (const ExportEntry& e : module->starExportEntries()) { // outer: E modules ... Rooted starNames(cx); if (!ModuleGetExportedNames(cx, requestedModule, exportStarSet, &starNames)) ... for (JSAtom* name : starNames) { // inner: S names if (name != cx->names().default_) { if (!ContainsElement(exportedNames, name)) { // O(N) scan over growing list if (!exportedNames.append(name)) ... } } } } ``` With E star-export modules each re-exporting S names, and the `exportedNames` list growing toward E×S items, the dedup cost is: ``` O(E × S × (E×S)) = O(E² × S²) ``` In real-world module bundles (e.g. barrel files re-exporting many libraries), E=10, S=100 gives 1 000 000 pointer comparisons per `GetExportedNames` call, versus O(E×S) = 1 000 with a HashSet. A secondary linear-scan defect exists in `GatherAvailableModuleAncestors`: ```cpp // line ~1918 if (!ContainsElement(execList, m)) { // O(L) per async parent module check execList.append(m); GatherAvailableModuleAncestors(cx, m, execList); // recursive } ``` `execList` is a `ModuleVector` (linear array). With L modules in execList and P async parent modules per call, and the recursion propagating the same list, the total cost is O(L×P) per invocation vs. O(P) with a HashSet guard. ## Fix Introduce a `HashSet` shadow set for the dedup check in `ModuleGetExportedNames`: ```cpp // In ModuleGetExportedNames, replace ContainsElement(exportedNames, name) with: // exportedNameSet.has(name) // and add exportedNameSet.put(name) on append. using ExportNameSet = GCHashSet, SystemAllocPolicy>; static bool ModuleGetExportedNames( JSContext* cx, Handle module, MutableHandle exportStarSet, MutableHandle exportedNames) { ... Rooted exportedNameSet(cx); // FIX: O(1) dedup guard // seed from already-known local/indirect exports: for (const ExportEntry& e : module->localExportEntries()) { if (!exportedNames.append(e.exportName())) return false; if (!exportedNameSet.put(e.exportName())) return false; } ... for (JSAtom* name : starNames) { if (name != cx->names().default_) { if (!exportedNameSet.has(name)) { // O(1) — fixed if (!exportedNames.append(name)) return false; if (!exportedNameSet.put(name)) return false; } } } ... } ``` For `GatherAvailableModuleAncestors`, pass a `HashSet` alongside `execList` or switch `execList` itself to a type that supports O(1) membership. ## Complexity | Path | Before | After | |------|--------|-------| | `ContainsElement(exportedNames, name)` | O(N) | O(1) | | Total for E=10, S=100 | ~1 000 000 ops | ~1 000 ops | | `GatherAvailableModuleAncestors` check | O(L) | O(1) | **Speedup:** ~1 000× at E=10, S=100 (barrel-file scenario) ## References - `ExportNameVector` typedef: `js/src/vm/ModuleObject.h` - `ModuleSet` is already a `GCHashSet` — same pattern can be applied to exportedNames - SM-0003: same file's `SimpleSet` pattern