java-topology/tools/tickets/defects/frrouting-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

3.3 KiB
Raw Blame History

id repo file line status severity complexity pattern
frrouting-0002 FRRouting/frr ospfd/ospf_spf.c 275 unpatched HIGH O(V²) worst case — V = vertices in OSPF area; triggered on every topology change listnode_lookup on children list inside Dijkstra main loop

Description

ospf_vertex_add_parent() is called for every vertex added to the SPF tree (inside the Dijkstra main loop at ospf_spf_calculate:1760). For each vertex v, it checks whether v is already in its parent's children list before adding it:

/* ospf_spf.c:264 */
static void ospf_vertex_add_parent(struct vertex *v)
{
    for (ALL_LIST_ELEMENTS_RO(v->parents, node, vp)) {
        /* No need to add two links from the same parent. */
        if (listnode_lookup(vp->parent->children, v) == NULL)  // O(|children|)
            listnode_add(vp->parent->children, v);
    }
}

listnode_lookup() is a linear scan over a singly-linked list (lib/linklist.c). vp->parent->children grows as the SPF tree is built; for the root vertex it reaches size = number of directly connected neighbors.

The Dijkstra loop (ospf_spf_calculate:17481772) processes V vertices. For each:

  • P ECMP parents (typically 1, max ~64)
  • Per parent: listnode_lookup over parent->children — O(|children| at that moment)

Worst case (star/hub-and-spoke topology, V routers all connected to one hub):

  • The hub's children list grows to size V
  • Each of V vertices calls listnode_lookup on that list
  • Total: O(V²)

Call context

ospf_spf_calculate() is called on every OSPF topology change (LSA received, neighbor state change, interface up/down). The scheduling is at ospf_spf_calculate_schedule() with configurable throttle timers (ospf_spf_calculate_schedule_worker:1844), but during convergence storms a large flat area will run this O(V²) loop repeatedly.

A flat enterprise OSPF area with 500 routers (common in large campus/DC deployments) has ~125,000 comparisons per SPF run vs. ~500 for the O(V) fix.

Fix

Replace struct list *children in struct vertex with a hash set for O(1) membership. FRRouting's lib/hash.h provides a hash table. Alternative: since OSPF vertex IDs are IPv4 addresses (4 bytes), a bitset indexed by some stable ID would give O(1) in O(V/64) space.

Minimal fix: parallel struct hash *children_set alongside the list (for compatibility with code that iterates children as a list), used only for the duplicate check:

/* Before */
if (listnode_lookup(vp->parent->children, v) == NULL)
    listnode_add(vp->parent->children, v);

/* After — O(1) duplicate check */
if (!hash_lookup(vp->parent->children_set, v)) {
    hash_get(vp->parent->children_set, v, hash_alloc_intern);
    listnode_add(vp->parent->children, v);
}

Or simpler: use SET_FLAG on the vertex's flags field to mark "already added as child of this parent" since each vertex is processed exactly once in Dijkstra.

Relationship to frrouting-0001

frrouting-0001 (already patched) fixed listnode_lookup × 5 in ospf_ti_lfa.c (the TI-LFA post-convergence fast-reroute calculator). This defect is in the primary SPF Dijkstra core — more fundamental, higher blast radius.

Work items

  • patch
  • unit test (SPF ops count: V=100, V=200, V=400 on hub-spoke topology)
  • integration test (OSPF convergence time before/after on large lab topology)