java-topology/docs/tickets/redis-0001-sinter-listpack-quadratic-membership.md
russell@unturf.com 9934133dcf whitepaper: 312 sites / 151 ecosystems — wave2+3 defect tables and PDF rebuild
Add 88 new defect entries to HIGH and MEDIUM tables:
  HIGH: mysql-0001/0002, mariadb-0001, redis-0001/0002, valkey-0001/0002, openvpn-0001,
        vlc-0001, prometheus-0001, otel-collector-0001, cockroachdb-0001..0004,
        tidb-0001..0008, kubernetes-0001/0002, go-0001, kotlin-0002, scala-0001,
        allegro5-0001, sdl2-0001, grafana-0001, clickhouse-0001, duckdb-0001,
        mongodb-0001, envoy-0001, istio-0001, cilium-0001, linkerd2-0001,
        linux-0001/0002/0003, tor-0002/0003, curl-0001, julia-0001, lua-0001,
        perl5-0001, nats-0001, spring-0003/0004, tomcat-0001, onos-0002, odl-0002

  MEDIUM: helm-0001, mariadb-0002, openssl-0001/0002, memcached-0001,
          cassandra-0001..0004, flink-0001, storm-0001/0002, zookeeper-0001..0003,
          pip-0001, gradle-0001, nginx-0001, haproxy-0001, caddy-0001, varnish-0001,
          ffmpeg-0001, gstreamer-0001, raylib-0001, love2d-0001, php-0001/0002,
          r-source-0001, cpython-0002, ruby-0001, rabbitmq-0003/0004, activemq-0001,
          ovs-0001, onos-0003, odl-0002, jetty-0001

PDF: 976K
2026-03-27 15:23:43 -04:00

2.4 KiB
Raw Blame History

redis-0001: SINTER/SDIFF on listpack sets — O(N×M) quadratic membership

Target: redis/redis Severity: HIGH CWE: CWE-407 (Inefficient Algorithmic Complexity) File: src/t_set.c:1472 (sinterGenericCommand) Status: PATCHED

Description

sinterGenericCommand iterates every element of the smallest set (outer loop, N elements) and for each element calls setTypeIsMemberAux on each of the remaining sets. When those sets use OBJ_ENCODING_LISTPACK (the default for sets with ≤ 128 entries, configured via set-max-listpack-entries), setTypeIsMemberAux dispatches to lpFind, which is an O(M) linear scan through the listpack blob.

Result: O(N × M) per intersection query instead of O(N) with a hash table.

At the default threshold of 128 entries:

  • Slow (listpack): 128 × 128 = 16,384 comparisons
  • Fast (hash table): 128 × O(1) = 128 comparisons
  • Worst-case speedup: 128×

The same quadratic path exists in sunionDiffGenericCommand DIFF algorithm 1 (comment in source confirms "O(N*M) where N is the size of the first set and M the number of sets") when inner sets are listpack-encoded.

Hot paths

  • SINTER key1 key2 [...] — intersection query
  • SINTERCARD numkeys key1 key2 [LIMIT n] — cardinality of intersection
  • SINTERSTORE dst key1 key2 [...] — store intersection
  • SDIFF key1 key2 (algo 1) — difference query when sets are small

Root cause

src/t_set.c:14741477:

while((encoding = setTypeNext(&si, &str, &len, &intobj)) != -1) {
    for (j = 1; j < setnum; j++) {
        if (!setTypeIsMemberAux(sets[j].set, str, len, intobj,
                                encoding == OBJ_ENCODING_HT))

setTypeIsMemberAux at encoding OBJ_ENCODING_LISTPACK calls lpFind, which walks the packed byte array from the beginning — O(M).

Fix

Before entering the intersection loop, convert any listpack-encoded set to a temporary hash table (dictCreate) so that subsequent membership checks are O(1). The temporary dict is freed after the loop. Sets already using OBJ_ENCODING_HT or OBJ_ENCODING_INTSET are left untouched.

See patch: defects/redis/patch/0001-sinter-listpack-promote-to-htset.patch

Also affects

  • valkey/valkey — identical sinterGenericCommand / sunionDiffGenericCommand code paths.

Ops ratio (unit test)

N=128, M=128:

  • slow ops: 16,384
  • fast ops: 128
  • speedup: 128×