From bbb8ad5552c72bdc5b9215560782dcf20996f845 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 29 Mar 2026 17:56:37 -0400 Subject: [PATCH] =?UTF-8?q?erlang-0004:=20edlin=5Ftype=5Fsuggestion=20prin?= =?UTF-8?q?t=5Ftype=20Visited=20list=20O(D=C2=B2);=20count=20642=E2=86=926?= =?UTF-8?q?43?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UNDF-REGISTRY.json | 3 +- .../erlang-0004-print-type-visited-sets.md | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 defects/erlang/patch/erlang-0004-print-type-visited-sets.md diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 7771d1b9e..1c50057fb 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -583,5 +583,6 @@ "quarkus-0003": "UNDF-2026-000000469", "micronaut-0004": "UNDF-2026-000000484", "starrocks-0002": "UNDF-2026-000000495", - "substrate-0003": "UNDF-2026-000000504" + "substrate-0003": "UNDF-2026-000000504", + "erlang-0004": "UNDF-2026-000000528" } diff --git a/defects/erlang/patch/erlang-0004-print-type-visited-sets.md b/defects/erlang/patch/erlang-0004-print-type-visited-sets.md new file mode 100644 index 000000000..c91c688a2 --- /dev/null +++ b/defects/erlang/patch/erlang-0004-print-type-visited-sets.md @@ -0,0 +1,92 @@ +# UNDF: UNDF-2026-000000528 +# UNDF: (pending) +# erlang-0004: edlin_type_suggestion print_type — Visited list O(D²) membership scan + +## CWE-407 — Algorithmic Complexity: O(D²) linear scan in type variable chain traversal + +| Field | Value | +|--------------|-------| +| ID | erlang-0004 | +| Severity | LOW | +| Ecosystem | erlang | +| Package | stdlib | +| File | `lib/stdlib/src/edlin_type_suggestion.erl` | +| Lines | 443–450 | +| Complexity | O(D²) total for type variable aliasing chain of depth D | +| Hot path | Erlang shell REPL autocompletion — called per tab-completion event | + +## Defect + +`print_type/4` resolves type variable constraints recursively. It tracks already-visited +type variables in `Visited`, a plain Erlang list. At each recursion step it scans +the entire list with `lists:member(Var, Visited)` — an O(D) scan — then prepends +the new variable with `[Var | Visited]`. For a chain of D aliased type variables +(A :: B, B :: C, C :: D, ...) the total scan cost is O(D²). + +```erlang +%% Line 443–450 (DEFECT: O(D) scan per step, O(D²) for chain depth D) +print_type({var, Name}=Var, Constraints, Visited, Options) -> + case lists:member(Var, Visited) of %% O(D) linear scan + true -> atom_to_list(Name); + false -> + case get_constraint(Var, Constraints) of + {constraint, _, T2} -> + print_type(T2, Constraints, [Var| Visited], Options); %% prepend + _ -> atom_to_list(Name) + end + end; +``` + +The two public wrappers initialize `Visited` to `[]`: + +```erlang +print_type(Type, Constraints) -> + lists:flatten(print_type(Type, Constraints, [], [])). +print_type(Type, Constraints, Options) -> + lists:flatten(print_type(Type, Constraints, [], Options)). +``` + +## Fix + +Replace the list-based `Visited` accumulator with an Erlang `sets` set. +The wrappers initialize `sets:new()`; the recursive clause uses +`sets:is_element/2` (O(1)) and `sets:add_element/2`. +All other `print_type/4` clauses pass `V` through unchanged — they are +unaffected by the type change. + +```erlang +%% AFTER — O(D) total +print_type(Type, Constraints) -> + lists:flatten(print_type(Type, Constraints, sets:new(), [])). +print_type(Type, Constraints, Options) -> + lists:flatten(print_type(Type, Constraints, sets:new(), Options)). + +print_type({var, Name}=Var, Constraints, Visited, Options) -> + case sets:is_element(Var, Visited) of %% O(1) + true -> atom_to_list(Name); + false -> + case get_constraint(Var, Constraints) of + {constraint, _, T2} -> + print_type(T2, Constraints, sets:add_element(Var, Visited), Options); + _ -> atom_to_list(Name) + end + end; +``` + +## Speedup + +| Chain depth (D) | Before (comparisons) | After (comparisons) | Speedup | +|----------------|---------------------|---------------------|---------| +| 10 | 55 | 10 | 5.5× | +| 50 | 1,275 | 50 | 25.5× | +| 100 | 5,050 | 100 | 50.5× | +| 1,000 | 500,500 | 1,000 | 500× | + +Growth before: O(D²). Growth after: O(D). + +## Notes + +- Practical D for type-variable aliasing chains is small (typically ≤ 5); the + structural defect exists regardless and can be triggered by crafted type specs. +- Severity is LOW: this path is only exercised during REPL shell autocompletion, + not in production Erlang applications.