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.
178 lines
5.3 KiB
ReStructuredText
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``
|