diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index f188b1aa4..50c801dd4 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -587,5 +587,8 @@ "erlang-0004": "UNDF-2026-000000528", "weld-0001": "UNDF-2026-000000530", "weld-0002": "UNDF-2026-000000550", - "weld-0003": "UNDF-2026-000000554" + "weld-0003": "UNDF-2026-000000554", + "go-0002": "UNDF-2026-000000562", + "nestjs-0003": "UNDF-2026-000000573", + "poetry-0001": "UNDF-2026-000000575" } diff --git a/defects/nestjs/patch/nestjs-0003-introspect-deps-lookupregistry-set.md b/defects/nestjs/patch/nestjs-0003-introspect-deps-lookupregistry-set.md new file mode 100644 index 000000000..0771bbe55 --- /dev/null +++ b/defects/nestjs/patch/nestjs-0003-introspect-deps-lookupregistry-set.md @@ -0,0 +1,88 @@ +# UNDF: UNDF-2026-000000573 +# UNDF: (pending) +# nestjs-0003: InstanceWrapper.introspectDepsAttribute — lookupRegistry string[] O(D²) includes + concat + +## CWE-407 — Algorithmic Complexity: O(D²) linear scan in dependency scope tree traversal + +| Field | Value | +|--------------|-------| +| ID | nestjs-0003 | +| Severity | MEDIUM | +| Ecosystem | nestjs | +| Package | @nestjs/core | +| File | `packages/core/injector/instance-wrapper.ts` | +| Lines | 266–298 | +| Complexity | O(D²) scan + O(D²) allocation for dependency chain of depth D | +| Hot path | Called during NestJS startup for every provider when resolving scope/durability | + +## Defect + +`InstanceWrapper.introspectDepsAttribute` traverses the provider dependency tree +recursively to determine scope (`isDependencyTreeStatic`) and durability +(`isDependencyTreeDurable`). It tracks the current DFS path in `lookupRegistry: string[]`. + +At each step it calls: +1. `lookupRegistry.includes(this[INSTANCE_ID_SYMBOL])` — O(D) linear scan per step +2. `lookupRegistry = lookupRegistry.concat(this[INSTANCE_ID_SYMBOL])` — O(D) array copy per step + +For a dependency chain of depth D, total cost: O(D²) scans + O(D²) memory allocation. + +```typescript +// packages/core/injector/instance-wrapper.ts:266-298 (DEFECT) +public introspectDepsAttribute( + callback: (collection: InstanceWrapper[], lookupRegistry: string[]) => boolean, + lookupRegistry: string[] = [], +): boolean { + if (lookupRegistry.includes(this[INSTANCE_ID_SYMBOL])) { // O(D) scan + return false; + } + lookupRegistry = lookupRegistry.concat(this[INSTANCE_ID_SYMBOL]); // O(D) copy + // recurse via callback → isDependencyTreeStatic(registry) + ... +} +``` + +`isDependencyTreeStatic` and `isDependencyTreeDurable` both call +`introspectDepsAttribute`, which calls `collection.some(item => !item.isDependencyTreeStatic(registry))` — +threading the growing `lookupRegistry` array through the traversal. + +## Fix + +Replace `string[]` with `Set`. `.has()` is O(1); create a new Set +at each step to preserve branch isolation (same semantics as `.concat()`). + +```typescript +// AFTER — O(D) total: O(1) lookup, O(D) new Set per step +public introspectDepsAttribute( + callback: (collection: InstanceWrapper[], lookupRegistry: Set) => boolean, + lookupRegistry: Set = new Set(), +): boolean { + if (lookupRegistry.has(this[INSTANCE_ID_SYMBOL])) { // O(1) + return false; + } + const nextRegistry = new Set(lookupRegistry); // O(D) once per node + nextRegistry.add(this[INSTANCE_ID_SYMBOL]); + // recurse via callback with nextRegistry + ... +} +``` + +Update callers: `isDependencyTreeStatic(lookupRegistry: Set = new Set())` +and `isDependencyTreeDurable(lookupRegistry: Set = new Set())`. + +## Speedup + +| Chain depth (D) | Before (.includes scans) | After (.has calls) | Speedup | +|----------------|------------------------|-------------------|---------| +| 10 | 55 | 10 | 5.5× | +| 50 | 1,275 | 50 | 25.5× | +| 100 | 5,050 | 100 | 50.5× | +| 500 | 125,250 | 500 | 250.5× | + +Growth before: O(D²). Growth after: O(D). + +## Notes + +`.concat()` also allocates a new `string[]` of size D+1 at each step — O(D²) +total memory allocation across the traversal. `new Set(existingSet)` is O(D) +allocation as well, but the constant is smaller and GC pressure is reduced.