# UNDF: UNDF-2026-000000262 From 0000000 Mon Sep 17 00:00:00 2001 Subject: [PATCH] acl: replace selector->patterns/channels lists with dicts for O(1) dedup CWE-407: ACLSetSelector performs O(P) listSearchKey to deduplicate key patterns when adding each ~ rule. When N rules are applied in sequence (e.g., ACL SETUSER user ~p1 ~p2 ... ~pN), total cost is O(1+2+...+N) = O(N²). Same defect applies to & rules using selector->channels. Trigger: ACL SETUSER myuser ~key:1 ~key:2 ... ~key:N ACL SETUSER myuser &ch:1 &ch:2 ... &ch:N ACL file load with many per-selector key/channel rules Severity: MEDIUM - Directly controllable by any client with ACL SETUSER permission - N=500 key patterns → 125,000 comparisons per SETUSER call - N=1000 → 500,000 comparisons Affected files: src/acl.c:1103 (listSearchKey selector->patterns) src/acl.c:1122 (listSearchKey selector->channels) Fix: replace selector->patterns and selector->channels lists with dicts (hash tables) during the dedup phase of ACLSetSelector. The existing list encoding is preserved for iteration / serialization by converting back to a list for ACLDescribeUser and related consumers. Alternatively (smaller diff): replace listSearchKey with dictFind using a sds-keyed dict maintained in parallel. Simpler targeted fix shown below: build a temporary hash set during ACLSetSelector and fall back to O(1) lookup. --- a/src/acl.c +++ b/src/acl.c @@ -349,6 +349,10 @@ aclSelector *ACLCreateSelector(int flags) { selector->patterns = listCreate(); selector->channels = listCreate(); + /* CWE-407 fix: parallel hash sets for O(1) dedup during ACLSetSelector */ + selector->patterns_ht = dictCreate(&sdsReplyDictType); + selector->channels_ht = dictCreate(&sdsReplyDictType); + listSetMatchMethod(selector->patterns,ACLListMatchKeyPattern); listSetFreeMethod(selector->patterns,ACLListFreeKeyPattern); listSetDupMethod(selector->patterns,ACLListDupKeyPattern); @@ -366,6 +370,8 @@ void ACLFreeSelector(aclSelector *selector) { listRelease(selector->patterns); listRelease(selector->channels); + dictRelease(selector->patterns_ht); + dictRelease(selector->channels_ht); + zfree(selector); } @@ -1099,10 +1103,12 @@ int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) { keyPattern *newpat = ACLKeyPatternCreate(sdsnewlen(op+offset,oplen-offset), flags); - listNode *ln = listSearchKey(selector->patterns,newpat); - /* Avoid re-adding the same key pattern multiple times. */ - if (ln == NULL) { + /* CWE-407 fix: O(1) dict lookup instead of O(P) listSearchKey */ + dictEntry *de = dictFind(selector->patterns_ht, newpat->pattern); + if (de == NULL) { listAddNodeTail(selector->patterns,newpat); + dictAdd(selector->patterns_ht, sdsdup(newpat->pattern), newpat); } else { - ((keyPattern *)listNodeValue(ln))->flags |= flags; + ((keyPattern *)dictGetVal(de))->flags |= flags; ACLKeyPatternFree(newpat); } selector->flags &= ~SELECTOR_FLAG_ALLKEYS; @@ -1117,10 +1123,12 @@ int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) { sds newpat = sdsnewlen(op+1,oplen-1); - listNode *ln = listSearchKey(selector->channels,newpat); - /* Avoid re-adding the same channel pattern multiple times. */ - if (ln == NULL) + /* CWE-407 fix: O(1) dict lookup instead of O(C) listSearchKey */ + if (dictFind(selector->channels_ht, newpat) == NULL) { listAddNodeTail(selector->channels,newpat); - else + dictAdd(selector->channels_ht, sdsdup(newpat), NULL); + } else { sdsfree(newpat); + } selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS; Speedup: O(N²) → O(N). N=500 patterns: 125,000 → 500 comparisons (250x). N=1000 patterns: 500,000 → 1,000 comparisons (500x). Also applies to Valkey (same code, src/acl.c:1217 and :1236).