111 lines
4.9 KiB
Markdown
111 lines
4.9 KiB
Markdown
# 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`
|
||
|
||
```c
|
||
// 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`
|
||
|
||
```c
|
||
// 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`
|
||
|
||
```c
|
||
// 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:
|
||
|
||
```c
|
||
// 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.
|