java-topology/tools/tickets/defects/erlang-0002.md
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

5.7 KiB

id repo file line severity status created updated
erlang-0002 erlang/otp lib/stdlib/src/digraph_utils.erl 495 MEDIUM FIXABLE-UPSTREAM 2026-03-23 2026-03-24

Defect

File: lib/stdlib/src/digraph_utils.erl:495 Pattern: lists:member(V, digraph:out_neighbours(G, V)) in is_reflexive_vertex/2 Complexity: O(degree(V)) per vertex → O(V²) aggregate over full graph Language: Erlang

Description

is_reflexive_vertex/2 checks whether a vertex has a self-loop:

is_reflexive_vertex(V, G) ->
    lists:member(V, digraph:out_neighbours(G, V)).

digraph:out_neighbours/2 builds a complete neighbor list by joining two ETS tables:

out_neighbours(G, V) ->
    ET = G#digraph.etab,
    NT = G#digraph.ntab,
    [V2 || {{out, _}, E} <- ets:lookup(NT, {out, V}),
           {E, _, V2, _} <- ets:lookup(ET, E)].

The ntab key is {out, V} — not {out, V, Neighbor}. There is no O(1) path to check "does V have a self-loop" from outside digraph.erl. You must build the full neighbor list and scan it.

is_reflexive_vertex is called from:

  • digraph_utils:loop_vertices/1 (line 294) — iterates all V, calls once per vertex
  • digraph_utils:is_simple/1 (line 485) — same pattern

Both call is_reflexive_vertex for every vertex in the graph → O(V²) total.

Why the originally proposed fix is wrong

The ticket previously proposed:

sets:is_element(V, NeighbourSet)

This does not help. You must first call digraph:out_neighbours(G, V) (which builds the list), then convert it with sets:from_list/1 (O(degree(V))), then do the O(1) lookup. Net cost: still O(degree(V)) per vertex, O(V²) total. No improvement.

Actual fix — add sltab to digraph.erl internals

digraph.erl uses three private ETS tables:

Table Key Value Purpose
vtab V {V, Label} vertex storage
etab E {E, V1, V2, Label} edge storage
ntab {out|in, V} {{out|in, V}, E} neighbor index

Add a fourth table: sltab (self-loop table)

sltab is a set storing V for every vertex that has at least one self-loop edge. Maintained entirely inside digraph.erl. No public API change required.

Changes to digraph.erl:

1. Record:

-record(digraph, {vtab = notable :: ets:table(),
                  etab = notable :: ets:table(),
                  ntab = notable :: ets:table(),
                  sltab = notable :: ets:table(),   %% NEW: self-loop index
                  cyclic = true  :: boolean()}).

2. new/1 — initialize sltab:

SL = ets:new(digraph_sltab, [set, public]),
set_type(Ts, #digraph{vtab=V, etab=E, ntab=N, sltab=SL});

3. delete/1 — destroy sltab:

ets:delete(G#digraph.sltab),

4. do_insert_edge/5 — record self-loops:

do_insert_edge(E, V1, V2, Label, #digraph{ntab=NT, etab=ET, sltab=SL}) ->
    ets:insert(NT, [{{out, V1}, E}, {{in, V2}, E}]),
    ets:insert(ET, {E, V1, V2, Label}),
    case V1 =:= V2 of
        true  -> ets:insert(SL, {V1});
        false -> ok
    end,
    E.

5. Edge deletion — remove self-loop record when last self-loop is deleted: In do_del_edge/2, after deleting from ntab/etab, check if any self-loop remains:

case {V1 =:= V2,
      ets:select(ET, [{{'_', V1, V1, '_'}, [], [true]}], 1)} of
    {true, '$end_of_table'} -> ets:delete(SL, V1);
    _ -> ok
end,

6. Export has_self_loop/2:

-spec has_self_loop(G, V) -> boolean() when G :: graph(), V :: vertex().
has_self_loop(G, V) ->
    ets:member(G#digraph.sltab, V).

7. digraph_utils:is_reflexive_vertex/2 — call the new O(1) function:

%% Before — O(degree(V))
is_reflexive_vertex(V, G) ->
    lists:member(V, digraph:out_neighbours(G, V)).

%% After — O(1)
is_reflexive_vertex(V, G) ->
    digraph:has_self_loop(G, V).

Complexity after fix

Operation Before After
is_reflexive_vertex/2 O(degree(V)) O(1)
loop_vertices/1 O(V²) O(V)
is_simple/1 (reflexive check) O(V²) O(V)
add_edge O(1) O(1) + 1 ETS insert
del_edge O(1) O(1) + 1 ETS select + conditional delete

Why this is load-bearing

digraph and digraph_utils are OTP stdlib — used by:

  • Erlang/OTP itself — release dependency resolution, application boot ordering
  • Rebar3 — build tool for all Erlang projects; dependency graph resolution
  • Mix / Hex — Elixir's build tool (calls into OTP digraph)
  • ejabberd — XMPP server used in production at scale; routing graph
  • RabbitMQ — message broker; exchange graph and routing topology
  • Every Erlang application that calls digraph_utils:loop_vertices/1 or is_simple/1 on a graph with self-loops

Any Erlang or Elixir application that checks graph reflexivity or simplicity on a non-trivial graph is running O(V²). For RabbitMQ or ejabberd routing graphs at scale, this is a live performance defect in production.

Status rationale

Previous status: NOT-FIXABLE (incorrect — the reason given was "OTP ABI constraint" but no specific constraint was identified).

Actual status: FIXABLE-UPSTREAM. The fix requires patching digraph.erl inside OTP stdlib. This is not a local fix — it requires an upstream OTP PR. The public API of digraph and digraph_utils is unchanged. has_self_loop/2 is a new addition.

Work items

  • Patch lib/stdlib/src/digraph.erl — add sltab, has_self_loop/2
  • Patch lib/stdlib/src/digraph_utils.erl:495 — use digraph:has_self_loop/2
  • Unit test — loop_vertices op count: V=100,200,400 on complete graph with self-loops
  • Integration test — is_simple/1, loop_vertices/1 correctness before/after
  • OTP upstream PR