CVE-2022-35977: Integer Overflow or Wraparound (CWE-190)
value: Redis is an in-memory database that persists on disk. Authenticated users issuing specially crafted `SETRANGE` and `SORT(_RO)` commands can trigger an integer overflow, resulting with Redis attempting to allocate impossible amounts of memory and abort with an out-of-memory (OOM) panic. The problem is fixed in Redis versions 7.0.8, 6.2.9 and 6.0.17. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product performs a calculation that can
         produce an integer overflow or wraparound when the logic
         assumes that the resulting value will always be larger than
         the original value. This occurs when an integer value is
         incremented to a value that is too large to store in the
         associated representation. When this occurs, the value may
         become a very small or negative number.
CVSS v3 base score: 5.5
Published: 2023-01-20T19:15Z
Repository: https://github.com/redis/redis
Language: Other
Files changed: src/sort.c, src/t_string.c, tests/unit/sort.tcl, tests/unit/type/string.tcl
Commit date: 2023-01-16 13:49:30 +0200
Commit message: Avoid integer overflows in SETRANGE and SORT (CVE-2022-35977) (#11720)

Authenticated users issuing specially crafted SETRANGE and SORT(_RO)
commands can trigger an integer overflow, resulting with Redis attempting
to allocate impossible amounts of memory and abort with an OOM panic.
Diff: diff --git a/src/sort.c b/src/sort.c
index 3132d17e1..77f4cbbc4 100644
--- a/src/sort.c
+++ b/src/sort.c
@@ -326,12 +326,14 @@ void sortCommandGeneric(client *c, int readonly) {
     case OBJ_SET: vectorlen =  setTypeSize(sortval); break;
     case OBJ_ZSET: vectorlen = dictSize(((zset*)sortval->ptr)->dict); break;
     default: vectorlen = 0; serverPanic("Bad SORT type"); /* Avoid GCC warning */
     }
 
-    /* Perform LIMIT start,count sanity checking. */
-    start = (limit_start < 0) ? 0 : limit_start;
+    /* Perform LIMIT start,count sanity checking.
+     * And avoid integer overflow by limiting inputs to object sizes. */
+    start = min(max(limit_start, 0), vectorlen);
+    limit_count = min(max(limit_count, -1), vectorlen);
     end = (limit_count < 0) ? vectorlen-1 : start+limit_count-1;
     if (start >= vectorlen) {
         start = vectorlen-1;
         end = vectorlen-2;
     }
diff --git a/src/t_string.c b/src/t_string.c
index 125f8ee86..6208d6a85 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -35,12 +35,18 @@ int getGenericCommand(client *c);
 
 /*-----------------------------------------------------------------------------
  * String Commands
  *----------------------------------------------------------------------------*/
 
-static int checkStringLength(client *c, long long size) {
-    if (!mustObeyClient(c) && size > server.proto_max_bulk_len) {
+static int checkStringLength(client *c, long long size, long long append) {
+    if (mustObeyClient(c))
+        return C_OK;
+    /* 'uint64_t' cast is there just to prevent undefined behavior on overflow */
+    long long total = (uint64_t)size + append;
+    /* Test configured max-bulk-len represending a limit of the biggest string object,
+     * and also test for overflow. */
+    if (total > server.proto_max_bulk_len || total < size || total < append) {
         addReplyError(c,"string exceeds maximum allowed size (proto-max-bulk-len)");
         return C_ERR;
     }
     return C_OK;
 }
@@ -452,11 +458,11 @@ void setrangeCommand(client *c) {
             addReply(c,shared.czero);
             return;
         }
 
         /* Return when the resulting string exceeds allowed size */
-        if (checkStringLength(c,offset+sdslen(value)) != C_OK)
+        if (checkStringLength(c,offset,sdslen(value)) != C_OK)
             return;
 
         o = createObject(OBJ_STRING,sdsnewlen(NULL, offset+sdslen(value)));
         dbAdd(c->db,c->argv[1],o);
     } else {
@@ -472,11 +478,11 @@ void setrangeCommand(client *c) {
             addReplyLongLong(c,olen);
             return;
         }
 
         /* Return when the resulting string exceeds allowed size */
-        if (checkStringLength(c,offset+sdslen(value)) != C_OK)
+        if (checkStringLength(c,offset,sdslen(value)) != C_OK)
             return;
 
         /* Create a copy when the object is shared or encoded. */
         o = dbUnshareStringValue(c->db,c->argv[1],o);
     }
@@ -701,12 +707,11 @@ void appendCommand(client *c) {
         if (checkType(c,o,OBJ_STRING))
             return;
 
         /* "append" is an argument, so always an sds */
         append = c->argv[2];
-        totlen = stringObjectLen(o)+sdslen(append->ptr);
-        if (checkStringLength(c,totlen) != C_OK)
+        if (checkStringLength(c,stringObjectLen(o),sdslen(append->ptr)) != C_OK)
             return;
 
         /* Append the value */
         o = dbUnshareStringValue(c->db,c->argv[1],o);
         o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
diff --git a/tests/unit/sort.tcl b/tests/unit/sort.tcl
index 7946211eb..7ef4fb1e5 100644
--- a/tests/unit/sort.tcl
+++ b/tests/unit/sort.tcl
@@ -337,6 +337,17 @@ start_server {
                 puts -nonewline "\n  Average time to sort: [expr double($elapsed)/100] milliseconds "
                 flush stdout
             }
         } {} {cluster:skip}
     }
+
+    test {SETRANGE with huge offset} {
+        r lpush L 2 1 0
+        # expecting a different outcome on 32 and 64 bit systems
+        foreach value {9223372036854775807 2147483647} {
+            catch {[r sort_ro L by a limit 2 $value]} res
+            if {![string match "2" $res] && ![string match "*out of range*" $res]} {
+                assert_not_equal $res "expecting an error or 2"
+            }
+        }
+    }
 }
diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl
index d04bdff32..36b349e09 100644
--- a/tests/unit/type/string.tcl
+++ b/tests/unit/type/string.tcl
@@ -596,6 +596,16 @@ start_server {tags {"string"}} {
     } {{{238 238} {239 239} 1} {{236 236} {238 238} 1} {{229 230} {236 237} 2} {{224 224} {235 235} 1} {{1 222} {13 234} 222}}
 
     test {LCS indexes with match len and minimum match len} {
         dict get [r LCS virus1{t} virus2{t} IDX WITHMATCHLEN MINMATCHLEN 5] matches
     } {{{1 222} {13 234} 222}}
+
+    test {SETRANGE with huge offset} {
+        foreach value {9223372036854775807 2147483647} {
+            catch {[r setrange K $value A]} res
+            # expecting a different error on 32 and 64 bit systems
+            if {![string match "*string exceeds maximum allowed size*" $res] && ![string match "*out of range*" $res]} {
+                assert_equal $res "expecting an error"
+           }
+        }
+    }
 }

Vulnerable code:     /* Perform LIMIT start,count sanity checking. */
    start = (limit_start < 0) ? 0 : limit_start;
static int checkStringLength(client *c, long long size) {
    if (!mustObeyClient(c) && size > server.proto_max_bulk_len) {
        if (checkStringLength(c,offset+sdslen(value)) != C_OK)
        if (checkStringLength(c,offset+sdslen(value)) != C_OK)
        totlen = stringObjectLen(o)+sdslen(append->ptr);
        if (checkStringLength(c,totlen) != C_OK)
Fixed code:     /* Perform LIMIT start,count sanity checking.
     * And avoid integer overflow by limiting inputs to object sizes. */
    start = min(max(limit_start, 0), vectorlen);
    limit_count = min(max(limit_count, -1), vectorlen);
static int checkStringLength(client *c, long long size, long long append) {
    if (mustObeyClient(c))
        return C_OK;
    /* 'uint64_t' cast is there just to prevent undefined behavior on overflow */
    long long total = (uint64_t)size + append;
    /* Test configured max-bulk-len represending a limit of the biggest string object,
     * and also test for overflow. */
    if (total > server.proto_max_bulk_len || total < size || total < append) {
        if (checkStringLength(c,offset,sdslen(value)) != C_OK)
        if (checkStringLength(c,offset,sdslen(value)) != C_OK)
        if (checkStringLength(c,stringObjectLen(o),sdslen(append->ptr)) != C_OK)

    test {SETRANGE with huge offset} {
        r lpush L 2 1 0
        # expecting a different outcome on 32 and 64 bit systems
        foreach value {9223372036854775807 2147483647} {
            catch {[r sort_ro L by a limit 2 $value]} res
            if {![string match "2" $res] && ![string match "*out of range*" $res]} {
                assert_not_equal $res "expecting an error or 2"
            }
        }
    }

    test {SETRANGE with huge offset} {
        foreach value {9223372036854775807 2147483647} {
            catch {[r setrange K $value A]} res
            # expecting a different error on 32 and 64 bit systems
            if {![string match "*string exceeds maximum allowed size*" $res] && ![string match "*out of range*" $res]} {
                assert_equal $res "expecting an error"
           }
        }
    }