120 lines
4 KiB
Markdown
120 lines
4 KiB
Markdown
# 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<JSAtom*, 0, SystemAllocPolicy>` (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<ExportNameVector> 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<JSAtom*>` 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<JSAtom*, DefaultHasher<JSAtom*>, SystemAllocPolicy>;
|
||
|
||
static bool ModuleGetExportedNames(
|
||
JSContext* cx, Handle<ModuleObject*> module,
|
||
MutableHandle<ModuleSet> exportStarSet,
|
||
MutableHandle<ExportNameVector> exportedNames) {
|
||
...
|
||
Rooted<ExportNameSet> 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<ModuleObject*>` 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
|