# UNDF: UNDF-2026-000000028 diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index a1b2c3d..f7c8d9e 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -5768,7 +5768,10 @@ static FnCallResult FnCallSetop(EvalContext *ctx, StringSet *set_b = StringSetNew(); if (!unique_mode) { JsonIterator iter = JsonIteratorInit(json_b); const JsonElement *e; while ((e = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) { StringSetAdd(set_b, xstrdup(JsonPrimitiveGetAsString(e))); } } + /* CWE-407 fix for unique() mode: build a StringSet from the input first + * so membership checks are O(1), then emit one Rlist per unique value. + * The defect: when unique_mode is true, set_b is always empty, so + * RlistAppendScalarIdemp() falls back to walking the growing returnlist + * on every call — O(N) per element, O(N²) total for N input values. + * Fix: use a dedicated StringSet to track seen values. O(N) total. */ + StringSet *seen = unique_mode ? StringSetNew() : NULL; + Rlist *returnlist = NULL; JsonIterator iter = JsonIteratorInit(json); const JsonElement *e; while ((e = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) { const char *value = JsonPrimitiveGetAsString(e); // Yes, this is an XOR. But it's more legible this way. if (!unique_mode && difference_mode && StringSetContains(set_b, value)) { continue; } if (!unique_mode && !difference_mode && !StringSetContains(set_b, value)) { continue; } - RlistAppendScalarIdemp(&returnlist, value); + if (unique_mode) + { + /* O(1) hash lookup replaces the O(N) Rlist walk. */ + if (!StringSetContains(seen, value)) + { + StringSetAdd(seen, xstrdup(value)); + RlistAppendScalar(&returnlist, value); + } + } + else + { + /* intersection / difference: set_b already deduplicates by + * construction; a given value can appear multiple times in + * json_a but set_b membership already filters correctly. + * Use Idemp here to preserve the previous dedup behaviour for + * the non-unique paths (they are not the hot path). */ + RlistAppendScalarIdemp(&returnlist, value); + } } JsonDestroyMaybe(json, allocated); if (json_b != NULL) { JsonDestroyMaybe(json_b, allocated_b); } + if (seen != NULL) + { + StringSetDestroy(seen); + } + StringSetDestroy(set_b); return (FnCallResult) { FNCALL_SUCCESS, (Rval) { returnlist, RVAL_TYPE_LIST } }; }