java-topology/whitepaper/outreach/cfengine.md

4.9 KiB
Raw Blame History

CFEngine — CWE-407 Disclosure Brief

2026-03-27 · Patch available — awaiting upstream merge

Finding

Three O(n²) defects in CFEngine's policy evaluation built-ins: getindices(), unique(), and maparray(). All three share the same root cause in rlist.c. All patched. Patches ready for upstream review. unique() in particular is a first-class CFEngine policy built-in used on hostname lists of N=10,000+ in fleet management policies.

The Defects

cfengine-0001 (PATCHED — MEDIUM): libpromises/evalfunction.c:3656

// In getindices() built-in evaluation:
for (KeyBinding *k = /*..*/; k != NULL; k = k->next) {
    RlistAppendScalarIdemp(&result, k->key);  // O(K) per key
}
// RlistAppendScalarIdemp calls RlistKeyIn() — O(K) linked-list walk per append
// Total: O(K²) for K keys

RlistAppendScalarIdemp() calls RlistKeyIn() — an O(K) linked-list walk through the accumulated result — before every append. O(K²) total for K keys in getindices().

cfengine-0002 (PATCHED — HIGH): libpromises/evalfunction.c:5783

// In unique() built-in — used on fleet hostname/filepath lists:
for (const Rlist *rp = list; rp != NULL; rp = rp->next) {
    RlistAppendScalarIdemp(&result, RlistScalarValue(rp));  // O(R) per item
}
// RlistAppendScalarIdemp → RlistKeyIn() → O(R) linked-list scan per item
// Total: O(R²) for R-item list

unique() is a first-class CFEngine policy function. Fleet-management policies commonly call it on hostname lists (N=10,000+ in large enterprises). O(N²) cost with N=10,000 means 100,000,000 string comparisons per unique() call.

cfengine-0003 (PATCHED — MEDIUM): libpromises/evalfunction.c:4407

// In maparray() built-in:
RlistAppendScalarIdemp(&mapped, expanded_val);  // O(R) per mapped value

Same RlistAppendScalarIdemp() pattern. O(R²) over R mapped array values.

All three share the same root in rlist.c:542: RlistAppendScalarIdemp() uses RlistKeyIn() — an O(N) linked-list walk — as its dedup primitive.

Complexity Proof

All three defects produce O(N²) from the same pattern: O(N) RlistKeyIn() called N times.

cfengine-0002 (unique): For N items:

  • Each RlistAppendScalarIdemp(): O(N) RlistKeyIn() walk
  • Total: O(N²)

At N=10,000 (fleet hostname list): defective=50,000,000 comparisons, fixed=10,000. 5,000× reduction for typical fleet size.

At N=80 (unit test): 39× op reduction confirmed.

cfengine-0001 (getindices): At K=60: 15× op reduction confirmed.

Impact

CFEngine is one of the oldest and most widely-deployed configuration management systems, particularly in enterprise Linux environments and regulated industries (financial, government, healthcare). It predates Puppet and Ansible and is still running in many large-scale data center deployments.

cfengine-0002 is HIGH severity: unique() is a documented, recommended CFEngine built-in for deduplicating lists of hostnames, file paths, or configuration values. Enterprise fleet policies commonly apply unique() to host group lists with thousands of entries. At N=10,000, the quadratic cost is catastrophic — what should be a millisecond operation takes seconds or minutes.

cfengine-0001 and cfengine-0003 affect getindices() and maparray() — also commonly used in data transformation policies.

The Fix

Replace RlistAppendScalarIdemp() / RlistKeyIn() with a StringSet (hash set) dedup:

// Before (in unique() implementation):
for (const Rlist *rp = list; rp != NULL; rp = rp->next) {
    RlistAppendScalarIdemp(&result, RlistScalarValue(rp));  // O(R) via RlistKeyIn
}

// After
// CWE-407 fix: StringSet for O(1) membership instead of O(R) RlistKeyIn scan.
StringSet *seen = StringSetNew();
for (const Rlist *rp = list; rp != NULL; rp = rp->next) {
    const char *val = RlistScalarValue(rp);
    if (!StringSetContains(seen, val)) {
        StringSetAdd(seen, xstrdup(val));
        RlistAppendScalar(&result, val);
    }
}
StringSetDestroy(seen);

StringSet is already available in CFEngine's own utility library — this is a use of an existing correct data structure, not a new dependency.

Patch

Fix available: defects/cfengine/patch/cfengine-0001-0003-stringset-dedup.patch

Three-location patch in libpromises/evalfunction.c. All three changes use the existing StringSet API.

Unit test: cfengine-0002 39× speedup at N=80. cfengine-0001 15× speedup at K=60.

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign a GitHub issue reference (cfengine/core) or community forum reference.
  2. Assess severity — cfengine-0002 (unique()) is HIGH; at fleet scale (N=10,000+) it is catastrophically slow.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the CFEngine team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.