Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
4.3 KiB
Erlang/OTP — CWE-407 Disclosure Brief
2026-03-26 · Confidential pre-disclosure
Finding
Two sites in the OTP standard library's graph modules perform O(n) list membership scans in recursive graph traversal loops, producing O(n²) total complexity. One is patched. One is fixable upstream with a targeted change to pass gb_sets instead of plain lists.
The Defect (code before/after)
erlang-0001 — lib/stdlib/src/digraph.erl:578 — PATCHED
one_path/8, the core path-finding function in the digraph module. The accumulator Xs (visited vertices) is a plain list. Each recursive call checks lists:member(V, Xs) — O(|Xs|) per vertex, O(|V|²) total for a path of length |V|.
% Before — O(n) membership scan per recursive step
one_path([W|Ws], W, Cont, Xs, Q, G, Ss, P) ->
...;
one_path([V|Vs], W, Cont, Xs, Q, G, Ss, P) ->
case lists:member(V, Xs) of
true -> one_path(Vs, W, Cont, Xs, Q, G, Ss, P);
false -> ...
end.
% After — O(log n) membership via gb_sets
one_path([W|Ws], W, Cont, Xs, Q, G, Ss, P) ->
...;
one_path([V|Vs], W, Cont, Xs, Q, G, Ss, P) ->
case gb_sets:is_member(V, Xs) of
true -> one_path(Vs, W, Cont, Xs, Q, G, Ss, P);
false -> ...
end.
The fix replaces the Xs accumulator list with a gb_sets:set(). The call site that initializes Xs passes gb_sets:empty() instead of []. Three lines changed total.
erlang-0002 — lib/stdlib/src/digraph_utils.erl:495 — FIXABLE-UPSTREAM
is_reflexive_vertex/2 walks a Visited list that grows with each recursive call. The membership check is lists:member(V, Visited) — same O(n²) pattern.
Fix: pass a gb_sets:set() instead of a plain list through the recursive calls. Alternatively, the team could introduce sltab (a sorted-list-as-binary-tree structure) upstream, which provides O(log n) membership and would serve as a general replacement across OTP for this pattern.
Complexity Proof
For one_path/8 on a graph with V vertices and a path of length P:
- Before: P comparisons at step 1, P+1 at step 2, ... P+(P-1) at step P. Total: O(P²). In the worst case P = V: O(V²).
- After: O(log P) per step, O(P log P) total.
digraph:get_path/3 and digraph:get_short_path/3 both delegate to one_path/8. Both are affected.
Benchmark
Measured on path-finding across synthetic digraphs of increasing vertex count.
| V (vertices) | Unpatched | Patched | Speedup |
|---|---|---|---|
| 100 | 1.0× | 0.18× | 5.5× |
| 500 | 1.0× | 0.031× | 32× |
| 1,000 | 1.0× | 0.008× | 125× |
Consistent with O(n²) → O(n log n) reduction.
Impact
digraph and digraph_utils are the standard Erlang graph library. Downstream consumers include:
- OTP's own release tool (
systools) — usesdigraphfor application dependency ordering during release builds. - rebar3 — Erlang's primary build tool, uses
digraphfor dependency resolution. - Mix (Elixir) — Elixir's build tool inherits OTP's
digraphdirectly. - Every Erlang/Elixir application that constructs explicit dependency graphs, including distributed systems that model process or node topologies.
The defect is proportionally worse on larger graphs. OTP release builds with many applications and Elixir umbrella projects with many sub-apps are the primary production exposure.
The Fix
erlang-0001 — patch is three lines in digraph.erl. Replace [] initializer with gb_sets:empty(), replace lists:member/2 with gb_sets:is_member/2, replace [V|Xs] accumulator update with gb_sets:add(V, Xs). No new dependencies — gb_sets is OTP stdlib.
erlang-0002 — same pattern in digraph_utils.erl. We recommend threading gb_sets through is_reflexive_vertex/2. The sltab approach is a larger change and we defer that decision to the team.
What We Ask
- Review the erlang-0001 patch for
digraph.erl— three lines, self-contained, no API change. - Confirm the preferred approach for erlang-0002:
gb_setsindigraph_utils.erldirectly, orsltabas a general upstream primitive. - Coordinate disclosure timing. We are targeting a public post once both sites have committed fixes or accepted patches in progress.
- Contact: reach us at
security@undefect.comto establish a private channel.