java-topology/defects/haproxy/patch/haproxy-0003-spoe-check-config-resolution-O-N2.md

3.6 KiB
Raw Blame History

UNDF: UNDF-2026-000000414

haproxy-0003 — flt_spoe.c spoe_check_config O(P×M), O(P×G), O(G×P×M) resolution loops

Ecosystem

haproxy (C)

Severity

LOW — config-finalization only, not hot path

Location

src/flt_spoe.c function spoe_check_config:

  • Line ~2407: list_for_each_entry(ph, &curmphs) × list_for_each_entry(msg, &curmsgs) — O(P×M)
  • Line ~2508: list_for_each_entry(ph, &curgphs) × list_for_each_entry_safe(grp, &curgrps) — O(P×G)
  • Line ~2526: list_for_each_entry(grp) × list_for_each_entry(ph, &grp->phs) × list_for_each_entry(msg, &curmsgs) — O(G×P×M)

Description

spoe_check_config is called after the SPOE config file is parsed to resolve placeholder references to their corresponding message/group objects. Three separate nested list-walk patterns are present:

Pattern 1 — placeholder-to-message resolution (lines ~24072504):

list_for_each_entry(ph, &curmphs, list) {           // outer: P placeholders
    list_for_each_entry(msg, &curmsgs, list) {       // inner: M messages
        if (strcmp(msg->id, ph->id) == 0) {          // O(1) strcmp
            // resolve
            goto next_mph;
        }
    }
    // error: undefined message
}

Complexity: O(P × M) where P = message placeholders, M = defined messages.

Pattern 2 — group-placeholder resolution (lines ~25082522):

list_for_each_entry(ph, &curgphs, list) {                // outer: P group placeholders
    list_for_each_entry_safe(grp, grpback, &curgrps, list) { // inner: G groups
        if (strcmp(grp->id, ph->id) == 0) {                   // O(1) strcmp
            goto next_aph;
        }
    }
}

Complexity: O(P × G) where P = group placeholders, G = defined groups.

Pattern 3 — group message assignment (lines ~25262553):

list_for_each_entry(grp, &curagent->groups, list) {         // outer: G groups
    list_for_each_entry_safe(ph, phback, &grp->phs, list) { // mid: P phs per group
        list_for_each_entry(msg, &curmsgs, list) {           // inner: M messages
            if (strcmp(msg->id, ph->id) == 0) {
                goto next_mph_grp;
            }
        }
        // error: undefined message
    }
}

Complexity: O(G × P × M) — cubic in terms of SPOE config size.

Note: haproxy-0002 covers the duplicate-detection loops at lines ~1580/1604/1991 (while *args[cur_arg] + list scan). This defect covers the distinct config-finalization resolution loops.

Complexity Table

Pattern Complexity Variables
msg placeholder resolution O(P × M) P=placeholders, M=messages
group placeholder resolution O(P × G) P=placeholders, G=groups
group-message assignment O(G × P × M) cubic

Typical SPOE configs: M=1050, G=520, P=1050. At M=50, G=20, P=50: pattern 3 = 50×50×50 = 125,000 iterations vs ~100 with maps.

Fix

Build a struct eb_root keyed by id from curmsgs and curgrps before the resolution loops, then replace each inner walk with an ebst_lookup:

struct eb_root msgs_by_id = EB_ROOT;
list_for_each_entry(msg, &curmsgs, list) {
    ebst_insert(&msgs_by_id, &msg->by_id_node);  // O(log M)
}

list_for_each_entry(ph, &curmphs, list) {
    msg = ebst_entry(ebst_lookup(&msgs_by_id, ph->id), struct spoe_message, by_id_node);
    if (!msg) { /* error */ goto error; }
    // resolve
}

HAProxy already uses ebst/ebmb extensively throughout the codebase.

CWE

CWE-407: Inefficient Algorithmic Complexity

Speedup

Pattern 3 at M=50, G=20, P=50: 125,000 → ~120 lookups (O(log M) each) ≈ 1000x.

Status

PATCHED (patch in this file)