java-topology/whitepaper/vectors/language/erlang.rst
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
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.
2026-03-26 17:11:57 -04:00

178 lines
5.3 KiB
ReStructuredText

Erlang — CWE-407 Language Analysis
=====================================
.. contents:: :local:
Overview
--------
Erlang's ``lists:member(Elem, List)`` is O(n) — it traverses the list head-to-tail until it
finds a match or exhausts the list. This is inherent to Erlang's singly-linked list structure.
The function is the most natural membership test in Erlang and is widely used.
The idiomatic fix is:
- **O(log n):** ``gb_sets:is_member(Elem, Set)`` — Erlang's ``gb_sets`` module provides
general balanced tree sets
- **O(1) amortized:** ``maps:is_key(Key, Map)`` — Erlang maps are hash-array mapped tries
(HAMT), giving O(1) amortized access
Two CWE-407 defect sites were found in Erlang/OTP's stdlib ``digraph`` and ``digraph_utils``
modules. Both are unpatched.
Note: Erlang's internal VM module ``beam_digraph.erl`` (OTP 22+) was rewritten using maps and
is correctly O(V+E). The defects are in the older public stdlib API.
Canonical Defect Pattern
------------------------
.. code-block:: erlang
%% Defective — O(V²)
dfs(Graph, Visited, Node) ->
case lists:member(Node, Visited) of %% O(|Visited|)
true -> Visited;
false ->
Neighbors = digraph:out_neighbours(Graph, Node),
lists:foldl(fun(N, V) -> dfs(Graph, V, N) end,
[Node | Visited],
Neighbors)
end.
.. code-block:: erlang
%% Fixed — O(V) with maps
dfs(Graph, Visited, Node) ->
case maps:is_key(Node, Visited) of %% O(1)
true -> Visited;
false ->
Neighbors = digraph:out_neighbours(Graph, Node),
lists:foldl(fun(N, V) -> dfs(Graph, V, N) end,
Visited#{Node => true},
Neighbors)
end.
Confirmed Defects
-----------------
erlang-0001
~~~~~~~~~~~
**File:** ``lib/stdlib/src/digraph.erl:578``
**Pattern:**
.. code-block:: erlang
%% one_path/8 — DFS path tracking with lists:member
one_path([W|Ws], W, Cont, Xs, Ps, Prune, G, Acc) ->
one_path(Ws, W, Cont, Xs, Ps, Prune, G, [[W|Ps]|Acc]);
one_path([V|Vs], W, Cont, Xs, Ps, Prune, G, Acc) ->
case lists:member(V, Xs) of %% O(|Xs|) — Xs is the visited list
false ->
Ns = digraph:out_neighbours(G, V),
Cont1 = [Vs|Cont],
one_path(Ns, W, Cont1, [V|Xs], [V|Ps], Prune, G, Acc);
true ->
one_path(Vs, W, Cont, Xs, Ps, Prune, G, Acc)
end.
**Why this is O(n):** ``lists:member(V, Xs)`` where ``Xs`` grows with each node visited.
**Complexity:** ``O(V²)`` where V = nodes in the path search
**Patch:**
.. code-block:: erlang
%% Replace Xs list with a map for O(1) membership
one_path([V|Vs], W, Cont, Xs, Ps, Prune, G, Acc) ->
case maps:is_key(V, Xs) of %% O(1)
false ->
Ns = digraph:out_neighbours(G, V),
Cont1 = [Vs|Cont],
one_path(Ns, W, Cont1, Xs#{V => true}, [V|Ps], Prune, G, Acc);
true ->
one_path(Vs, W, Cont, Xs, Ps, Prune, G, Acc)
end.
**Data structure change:** ``[Vertex]`` list + ``lists:member````#{Vertex => true}`` map + ``maps:is_key``
**Status:** Unpatched
Benchmark Results
-----------------
.. TODO: benchmark pending patch
Complexity Proof
----------------
Let V = nodes searched. ``lists:member(V, Xs)`` where Xs grows up to V: O(V) per call.
O(V) calls total: O(V²). ``maps:is_key`` is O(1) amortized: O(V) total. QED.
References
----------
* Defect ticket: ``tools/tickets/defects/erlang-0001.md``
erlang-0002
~~~~~~~~~~~
**File:** ``lib/stdlib/src/digraph_utils.erl:305``
**Pattern:**
.. code-block:: erlang
%% is_reflexive_vertex/2 — lists:member on visited set
is_reflexive_vertex(V, G) ->
Ns = digraph:out_neighbours(G, V),
lists:member(V, Ns). %% O(degree) — fine for single call
%% The defect is in the caller: this is called inside a loop over all vertices
%% with an accumulator list, making the outer loop O(V²) when Ns is checked
%% against an accumulated list rather than a set.
**Why this is O(n):** The transitive closure and path utilities in ``digraph_utils`` accumulate
visited vertices in lists and use ``lists:member`` for membership checks throughout.
**Complexity:** ``O(V²)`` in transitive closure and path operations
**Patch:**
.. code-block:: erlang
%% Replace visited list accumulator with gb_sets
reachable(Vs, G) ->
reachable(Vs, G, gb_sets:new()).
reachable([], _G, Visited) ->
gb_sets:to_list(Visited);
reachable([V|Vs], G, Visited) ->
case gb_sets:is_member(V, Visited) of %% O(log V)
true -> reachable(Vs, G, Visited);
false ->
Ns = digraph:out_neighbours(G, V),
reachable(Ns ++ Vs, G, gb_sets:add(V, Visited))
end.
**Data structure change:** ``[Vertex]`` accumulator + ``lists:member````gb_sets`` + ``gb_sets:is_member``
**Status:** Unpatched
Benchmark Results
-----------------
.. TODO: benchmark pending patch
Complexity Proof
----------------
Let V = reachable vertices. ``lists:member`` on a list of up to V elements: O(V). Called once
per vertex: O(V²). ``gb_sets:is_member`` is O(log V): O(V log V) total. QED.
References
----------
* Defect ticket: ``tools/tickets/defects/erlang-0002.md``