# UNDF: UNDF-2026-000000029 diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index a1b2c3d..c2e1f7b 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -4221,6 +4221,12 @@ static FnCallResult FnCallMapData(EvalContext *ctx, ARG_UNUSED const Policy *pol bool mapdatamode = (strcmp(fp->name, "mapdata") == 0); Rlist *returnlist = NULL; + /* CWE-407 fix: track already-appended values in a StringSet so the + * nested-container branch can dedup in O(1) per element instead of the + * O(R) linked-list walk performed by RlistAppendScalarIdemp. + * With N total sub-elements, that was O(N²); with StringSet it is O(N). + * Initialised here; destroyed at every exit path below. */ + StringSet *seen = StringSetNew(); + // This is a delayed evaluation function, so we have to resolve arguments ourselves // We resolve them once now, to get the second or third argument with the iteration data Rlist *expargs = NewExpArgs(ctx, policy, fp, NULL); @@ -4335,6 +4341,7 @@ static FnCallResult FnCallMapData(EvalContext *ctx, ARG_UNUSED const Policy *pol if (strstr(BufferData(expbuf), "$(this.k)") || strstr(BufferData(expbuf), "${this.k}") || strstr(BufferData(expbuf), "$(this.v)") || strstr(BufferData(expbuf), "${this.v}")) { + StringSetDestroy(seen); RlistDestroy(returnlist); EvalContextVariableRemoveSpecial(ctx, SPECIAL_SCOPE_THIS, "k"); EvalContextVariableRemoveSpecial(ctx, SPECIAL_SCOPE_THIS, "v"); @@ -4385,6 +4392,7 @@ static FnCallResult FnCallMapData(EvalContext *ctx, ARG_UNUSED const Policy *pol if (strstr(BufferData(expbuf), "$(this.k)") || strstr(BufferData(expbuf), "${this.k}") || (havekey && (strstr(BufferData(expbuf), "$(this.k[1])") || strstr(BufferData(expbuf), "${this.k[1]}"))) || strstr(BufferData(expbuf), "$(this.v)") || strstr(BufferData(expbuf), "${this.v}")) { + StringSetDestroy(seen); RlistDestroy(returnlist); EvalContextVariableRemoveSpecial(ctx, SPECIAL_SCOPE_THIS, "k"); if (havekey) @@ -4404,9 +4413,18 @@ static FnCallResult FnCallMapData(EvalContext *ctx, ARG_UNUSED const Policy *pol if (canonifymode) { BufferCanonify(expbuf); } - RlistAppendScalarIdemp(&returnlist, BufferData(expbuf)); + /* CWE-407 fix: O(1) hash membership test replaces O(R) list + * walk. RlistAppendScalarIdemp called RlistKeyIn() which + * scanned the full returnlist on every iteration. */ + const char *expanded = BufferData(expbuf); + if (!StringSetContains(seen, expanded)) + { + StringSetAdd(seen, xstrdup(expanded)); + RlistAppendScalar(&returnlist, expanded); + } if (havekey) { EvalContextVariableRemoveSpecial(ctx, SPECIAL_SCOPE_THIS, "k[1]"); @@ -4421,6 +4439,8 @@ static FnCallResult FnCallMapData(EvalContext *ctx, ARG_UNUSED const Policy *pol } BufferDestroy(expbuf); + StringSetDestroy(seen); + JsonDestroyMaybe(container, allocated); RlistDestroy(expargs);