Memory Safety / Integer overflow in zrange/store path (negative length handling)

MEDIUM
redis/redis
Commit: bad33f8738b4
Affected: <= 8.6.2
2026-04-04 12:39 UTC

Description

The commit fixes a data type conversion bug in zrangeResultBeginStore where a length of -1 (meaning unknown) could be passed through to zsetTypeCreate. After a prior change (#12185), -1 would be interpreted as SIZE_MAX, leading to a pathological path in dictExpand and potentially causing memory safety issues or incorrect encoding (dst key switching from listpack to skiplist). The fix clamps negative lengths to 0 to avoid passing -1 into zsetTypeCreate, improving memory safety and correctness in zrangestore-related paths (including cases where the zrangestore source does not exist or BYSCORE/BYLEX is used). This is a genuine vulnerability fix rather than a mere dependency bump or cleanup.

Proof of Concept

PoC (conceptual, reproduce on a vulnerable build before this patch): Prerequisites: - A Redis build prior to this patch (e.g., 8.6.2 before the fix) where the bug is present. - A vulnerable environment where zrangestore can be invoked with a source key that does not exist or where BYSCORE/BYLEX is used to drive the path that passes a length of -1 to the internal emitter. Reproduction steps: 1) Start Redis (unpatched build). 2) Ensure the destination key does not exist (optional). 3) Trigger zrangestore with a non-existent source using -1 length semantics: redis-cli> ZRANGESTORE dst missing 0 -1 BYSCORE # or for BYLEX: redis-cli> ZRANGESTORE dst missing 0 -1 BYLEX Expected (pre-patch behavior): - The internal code path may pass length = -1 to zsetTypeCreate, which converts -1 to SIZE_MAX and attempts a dictExpand with an enormous size. This can lead to memory allocation failure, potential crash, or unintended encoding changes (dst key could end up using skiplist encoding instead of listpack). Expected (post-patch behavior): - The code clamps negative length to 0 in zrangeResultBeginStore, so no overflow occurs. The destination key will be created with 0 elements if the source is missing, and encoding remains consistent without triggering pathological allocations. Notes: - The exact observable symptoms in a crash-prone build may vary (crash, OOM, or corruption) depending on allocator behavior and memory state. - This PoC targets the vulnerable path where -1 length could propagate to zsetTypeCreate before the fix; after the fix, the path is guarded.

Commit Details

Author: Yanqi Lv

Date: 2024-03-19 06:52 UTC

Message:

fix wrong data type conversion in zrangeResultBeginStore (#13148) In `beginResultEmission`, -1 means the result length is not known in advance. But after #12185, if we pass -1 to `zrangeResultBeginStore`, it will convert to SIZE_MAX in `zsetTypeCreate` and try to `dictExpand`. Although `dictExpand` won't succeed because the size overflows, I think we'd better to avoid this wrong conversion. This bug can be triggered when the source of `zrangestore` doesn't exist or we use `zrangestore` command with `byscore` or `bylex`. The impact is that dst keys will be converted to use skiplist instead of listpack.

Triage Assessment

Vulnerability Type: Memory Safety / Overflow

Confidence: MEDIUM

Reasoning:

The change prevents a negative length from being passed into zsetTypeCreate, which would convert -1 to an unsigned very large value and potentially trigger a pathological memory/structure overflow during zrange/store handling. This improves memory safety and correctness in zrangestore-related paths, reducing risk of improper representation (e.g., skiplist vs listpack) due to overflow. While primarily a correctness fix, it mitigates a potential overflow/memory-safety issue that could be exploited in edge cases.

Verification Assessment

Vulnerability Type: Memory Safety / Integer overflow in zrange/store path (negative length handling)

Confidence: MEDIUM

Affected Versions: <= 8.6.2

Code Diff

diff --git a/src/t_zset.c b/src/t_zset.c index 3a5338067ac..6d4edd2123d 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1234,7 +1234,8 @@ unsigned long zsetLength(const robj *zobj) { * and the value len hint indicates the approximate individual size of the added elements, * they are used to determine the initial representation. * - * If the hints are not known, and underestimation or 0 is suitable. */ + * If the hints are not known, and underestimation or 0 is suitable. + * We should never pass a negative value because it will convert to a very large unsigned number. */ robj *zsetTypeCreate(size_t size_hint, size_t val_len_hint) { if (size_hint <= server.zset_max_listpack_entries && val_len_hint <= server.zset_max_listpack_value) @@ -3073,7 +3074,7 @@ static void zrangeResultFinalizeClient(zrange_result_handler *handler, /* Result handler methods for storing the ZRANGESTORE to a zset. */ static void zrangeResultBeginStore(zrange_result_handler *handler, long length) { - handler->dstobj = zsetTypeCreate(length, 0); + handler->dstobj = zsetTypeCreate(length >= 0 ? length : 0, 0); } static void zrangeResultEmitCBufferForStore(zrange_result_handler *handler, diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index 0290acbbf98..dc0554d8411 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -2286,12 +2286,18 @@ start_server {tags {"zset"}} { } {b 2 c 3} test {ZRANGESTORE BYLEX} { + set res [r zrangestore z3{t} z1{t} \[b \[c BYLEX] + assert_equal $res 2 + assert_encoding listpack z3{t} set res [r zrangestore z2{t} z1{t} \[b \[c BYLEX] assert_equal $res 2 r zrange z2{t} 0 -1 withscores } {b 2 c 3} test {ZRANGESTORE BYSCORE} { + set res [r zrangestore z4{t} z1{t} 1 2 BYSCORE] + assert_equal $res 2 + assert_encoding listpack z4{t} set res [r zrangestore z2{t} z1{t} 1 2 BYSCORE] assert_equal $res 2 r zrange z2{t} 0 -1 withscores
← Back to Alerts View on GitHub →