wave17: 562/240 — vim/qemu/tcl/nodejs/bun/httpd-0002/systemd/emacs
This commit is contained in:
parent
cce7ec653a
commit
dde5ec97fb
13 changed files with 887 additions and 4 deletions
68
defects/bun/patch/bun-0001-bin-folders-dedup-set.md
Normal file
68
defects/bun/patch/bun-0001-bin-folders-dedup-set.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# bun-0001 — CWE-407 in dirInfoUncached bin_folders dedup
|
||||
|
||||
**Severity:** MEDIUM
|
||||
**File:** `src/resolver/resolver.zig`
|
||||
**Symbol:** `dirInfoUncached` — `for (bin_folders.constSlice()) |existing_folder|`
|
||||
|
||||
## Defect
|
||||
|
||||
During module resolution, `dirInfoCachedMaybeLog` walks the directory tree from
|
||||
the target path up to the filesystem root, calling `dirInfoUncached` for each
|
||||
uncached directory level (up to D levels).
|
||||
|
||||
Inside `dirInfoUncached`, when a `.bin` directory is found under `node_modules`,
|
||||
the path is deduplicated against a shared `bin_folders` array using a linear
|
||||
scan before appending:
|
||||
|
||||
```zig
|
||||
// Called inside while (queue_slice.len > 0) loop — D iterations
|
||||
fn dirInfoUncached(...) {
|
||||
// ...
|
||||
for (bin_folders.constSlice()) |existing_folder| { // O(B) scan
|
||||
if (strings.eql(existing_folder, bin_path)) {
|
||||
break :append_bin_dir;
|
||||
}
|
||||
}
|
||||
bin_folders.append(...); // B grows up to D
|
||||
}
|
||||
```
|
||||
|
||||
`bin_folders` is a shared array that accumulates all `.bin` folder paths found
|
||||
across the entire directory tree traversal. Each call to `dirInfoUncached`
|
||||
performs an O(B) scan where B grows with each new `.bin` directory found.
|
||||
With D directory levels each contributing one `.bin` entry, total dedup cost is
|
||||
**O(D²)**.
|
||||
|
||||
In a monorepo with deep nesting (D=50 directories, each with `node_modules/.bin`),
|
||||
this generates ~1,250 comparisons per `require()` call instead of ~50.
|
||||
|
||||
## Fix
|
||||
|
||||
Replace `bin_folders` linear scan with a `std.StringHashMap` or
|
||||
`std.BufSet` for O(1) membership:
|
||||
|
||||
```zig
|
||||
var bin_folders_set = std.StringHashMap(void).init(allocator);
|
||||
defer bin_folders_set.deinit();
|
||||
|
||||
// In the dedup check:
|
||||
const gop = try bin_folders_set.getOrPut(bin_path);
|
||||
if (!gop.found_existing) {
|
||||
bin_folders.append(stored_path) catch {};
|
||||
}
|
||||
```
|
||||
|
||||
## Complexity
|
||||
|
||||
| Scenario | Before | After |
|
||||
|---|---|---|
|
||||
| D=50 nested node_modules dirs | O(D²) = ~1,250 ops | O(D) = 50 ops |
|
||||
| D=200 deep monorepo | O(D²) = ~20,000 ops | O(D) = 200 ops |
|
||||
| Ratio at D=200 | ~100× overhead | 1× |
|
||||
|
||||
## Source location
|
||||
|
||||
`src/resolver/resolver.zig` — `fn dirInfoUncached`, in the
|
||||
`if (r.care_about_bin_folder)` / `append_bin_dir` blocks (~lines 4024–4073),
|
||||
called from within the `while (queue_slice.len > 0)` loop in
|
||||
`dirInfoCachedMaybeLog` (~line 2956).
|
||||
164
defects/bun/unit/BunBinFoldersTest.java
Normal file
164
defects/bun/unit/BunBinFoldersTest.java
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package unit;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Unit test for bun-0001: CWE-407 in dirInfoUncached bin_folders dedup.
|
||||
*
|
||||
* bun-0001 (MEDIUM):
|
||||
* File: src/resolver/resolver.zig ~lines 4041-4073
|
||||
* Symbol: dirInfoUncached — for (bin_folders.constSlice()) |existing_folder|
|
||||
* Defect: Module resolution walks D directory levels. For each level that
|
||||
* contains a node_modules/.bin directory, the path is deduplicated against a
|
||||
* shared bin_folders array using a linear scan: O(B) per call where B grows.
|
||||
* With D levels each contributing one entry: O(D²) total dedup cost.
|
||||
* Fix: Use a HashSet for O(1) membership: O(D) total.
|
||||
*
|
||||
* Modeled here in Java:
|
||||
* Zig slice linear scan ≡ List<String>.contains() (defective)
|
||||
* Zig StringHashMap ≡ java.util.Set<String> (fixed)
|
||||
* Comparison counts tracked at the membership-test site.
|
||||
*
|
||||
* Expected at D=200 directory levels:
|
||||
* defective comparisons ~ D*(D-1)/2 ≈ 19,900
|
||||
* fixed comparisons ~ D = 200
|
||||
* ratio > 50×
|
||||
*/
|
||||
public class BunBinFoldersTest {
|
||||
|
||||
// =========================================================================
|
||||
// Model: dirInfoUncached bin_folders dedup
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Defective: bin_folders is a plain List; dedup is a linear scan per entry.
|
||||
*
|
||||
* Simulates: dirInfoCachedMaybeLog calls dirInfoUncached D times, once per
|
||||
* directory level. Each call finds a unique .bin path and deduplicates it
|
||||
* against the growing bin_folders accumulator.
|
||||
*/
|
||||
static long deduplicateDefective(int D) {
|
||||
long comparisons = 0;
|
||||
List<String> binFolders = new ArrayList<>();
|
||||
|
||||
for (int level = 0; level < D; level++) {
|
||||
String binPath = "/project/level_" + level + "/node_modules/.bin";
|
||||
// for (bin_folders.constSlice()) |existing| — O(binFolders.size())
|
||||
boolean found = false;
|
||||
for (String existing : binFolders) {
|
||||
comparisons++;
|
||||
if (existing.equals(binPath)) { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
binFolders.add(binPath);
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed: HashSet companion for O(1) membership; same accumulated list.
|
||||
*/
|
||||
static long deduplicateFixed(int D) {
|
||||
long comparisons = 0;
|
||||
List<String> binFolders = new ArrayList<>();
|
||||
Set<String> binFoldersSet = new HashSet<>();
|
||||
|
||||
for (int level = 0; level < D; level++) {
|
||||
String binPath = "/project/level_" + level + "/node_modules/.bin";
|
||||
// binFoldersSet.contains(binPath) — O(1)
|
||||
comparisons++;
|
||||
if (!binFoldersSet.contains(binPath)) {
|
||||
binFoldersSet.add(binPath);
|
||||
binFolders.add(binPath);
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Tests
|
||||
// =========================================================================
|
||||
|
||||
/** Test 1 — Correctness: both produce the same accumulated bin_folders list. */
|
||||
static void testCorrectnessMatch() {
|
||||
int D = 20;
|
||||
List<String> defList = new ArrayList<>();
|
||||
List<String> fixList = new ArrayList<>();
|
||||
Set<String> fixSet = new HashSet<>();
|
||||
|
||||
for (int level = 0; level < D; level++) {
|
||||
String binPath = "/project/level_" + level + "/node_modules/.bin";
|
||||
if (!defList.contains(binPath)) defList.add(binPath);
|
||||
if (!fixSet.contains(binPath)) { fixSet.add(binPath); fixList.add(binPath); }
|
||||
}
|
||||
|
||||
assert defList.size() == D
|
||||
: "expected " + D + " bin folders; got " + defList.size();
|
||||
assert new HashSet<>(defList).equals(fixSet)
|
||||
: "defective and fixed bin_folders sets differ";
|
||||
|
||||
System.out.println("PASS testCorrectnessMatch");
|
||||
}
|
||||
|
||||
/** Test 2 — bun-0001: defective is O(D²), fixed is O(D). */
|
||||
static void testRatioAtScale() {
|
||||
int D = 200;
|
||||
|
||||
long defComp = deduplicateDefective(D);
|
||||
long fixComp = deduplicateFixed(D);
|
||||
double ratio = (double) defComp / fixComp;
|
||||
|
||||
// Defective: all D paths are unique; accumulator grows 0, 1, ..., D-1.
|
||||
// Sum of scans: 0+1+...+(D-1) = D*(D-1)/2.
|
||||
long expectedDef = (long) D * (D - 1) / 2;
|
||||
assert defComp == expectedDef
|
||||
: "defective comparisons should be D*(D-1)/2=" + expectedDef
|
||||
+ "; got " + defComp;
|
||||
|
||||
// Fixed: exactly D probes (one Set.contains per level).
|
||||
assert fixComp == D
|
||||
: "fixed comparisons should be D=" + D + "; got " + fixComp;
|
||||
|
||||
assert ratio >= 50.0
|
||||
: "ratio should be >=50x at D=200; got " + ratio;
|
||||
|
||||
System.out.printf(
|
||||
"PASS testRatioAtScale (defective=%d, fixed=%d, ratio=%.1fx)%n",
|
||||
defComp, fixComp, ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 3 — Duplicate bin paths (same .bin dir encountered multiple times):
|
||||
* dedup correctly suppresses re-adding the same path in both implementations.
|
||||
*/
|
||||
static void testWithDuplicateBinPaths() {
|
||||
int D = 40; // 40 levels but only 20 unique .bin dirs (every 2 levels share one)
|
||||
List<String> defList = new ArrayList<>();
|
||||
Set<String> fixSet = new HashSet<>();
|
||||
List<String> fixList = new ArrayList<>();
|
||||
|
||||
for (int level = 0; level < D; level++) {
|
||||
String binPath = "/project/level_" + (level / 2) + "/node_modules/.bin";
|
||||
if (!defList.contains(binPath)) defList.add(binPath);
|
||||
if (!fixSet.contains(binPath)) { fixSet.add(binPath); fixList.add(binPath); }
|
||||
}
|
||||
|
||||
int expectedUnique = D / 2;
|
||||
assert defList.size() == expectedUnique
|
||||
: "expected " + expectedUnique + " unique bin dirs; got " + defList.size();
|
||||
assert new HashSet<>(defList).equals(fixSet)
|
||||
: "duplicate-input bin_folders sets differ";
|
||||
|
||||
System.out.println("PASS testWithDuplicateBinPaths");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
public static void main(String[] args) {
|
||||
testCorrectnessMatch();
|
||||
testRatioAtScale();
|
||||
testWithDuplicateBinPaths();
|
||||
System.out.println("All bun-0001 tests passed.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue