Repository: redis/redis
Pull request #14028: Fix keysizes -  SPOP with count (case 3) and SETRANGE
Author: login: moticless
Created: 2025-05-11T07:18:58Z | Merged: 2025-05-19T13:59:22Z

This PR addresses issues with the keysizes histogram tracking in two Redis commands:

**SPOP with count (case 3)**
In the spopWithCountCommand function, when handling case 3 (where the number of elements to return is very large, approaching the size of the set itself), the keysizes histogram was not being properly updated. This PR adds the necessary call to updateKeysizesHist() to ensure the histogram accurately reflects the changes in set size after the operation.

**SETRANGE command**
Fixed an issue in the setrangeCommand function where the keysizes histogram wasn't being properly updated when modifying strings. The PR ensures that the histogram correctly tracks the old and new lengths of the string after a SETRANGE operation.

Added tests accordingly. 


Diff:
diff --git a/src/db.c b/src/db.c
index f19505292de..e8dd76b08b1 100644
--- a/src/db.c
+++ b/src/db.c
@@ -40,7 +40,7 @@ typedef enum {
 } keyStatus;
 
 static keyStatus expireIfNeeded(redisDb *db, robj *key, kvobj *kv, int flags);
-static void dbSetValue(redisDb *db, robj *key, robj **valref, int overwrite, dictEntryLink link);
+static void dbSetValue(redisDb *db, robj *key, robj **valref, dictEntryLink link, int overwrite, int updateKeySizes);
 
 /* Update LFU when an object is accessed.
  * Firstly, decrement the counter if the decrement time is reached.
@@ -122,6 +122,46 @@ void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, in
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * Triggered by DEBUG KEYSIZES-HIST-ASSERT 1 and tested after each command.
+ */
+void dbgAssertKeysizesHist(redisDb *db) {
+    /* Scan DB and build expected histogram by scanning all keys */
+    int64_t scanHist[MAX_KEYSIZES_TYPES][MAX_KEYSIZES_BINS] = {{0}};
+    dictEntry *de;
+    kvstoreIterator *kvs_it = kvstoreIteratorInit(db->keys);
+    while ((de = kvstoreIteratorNext(kvs_it)) != NULL) {
+        kvobj *kv = dictGetKV(de);
+        if (kv->type < OBJ_TYPE_BASIC_MAX) {
+            int64_t len = getObjectLength(kv);
+            scanHist[kv->type][(len == 0) ? 0 : log2ceil(len) + 1]++;
+        }
+    }
+    kvstoreIteratorRelease(kvs_it);
+    for (int type = 0; type < OBJ_TYPE_BASIC_MAX; type++) {
+        volatile int64_t *keysizesHist = kvstoreGetMetadata(db->keys)->keysizes_hist[type];
+        for (int i = 0; i < MAX_KEYSIZES_BINS; i++) {
+            if (scanHist[type][i] == keysizesHist[i])
+                continue;
+
+            /* print scanStr vs. expected histograms for debugging */
+            char scanStr[500], keysizesStr[500];
+            int l1 = 0, l2 = 0;
+            for (int j = 0; (j < MAX_KEYSIZES_BINS) && (l1 < 500) && (l2 < 500); j++) {
+                if (scanHist[type][j])
+                    l1 += snprintf(scanStr + l1, sizeof(scanStr) - l1,
+                                        "[%d]=%"PRId64" ", j, scanHist[type][j]);
+                if (keysizesHist[j])
+                    l2 += snprintf(keysizesStr + l2, sizeof(keysizesStr) - l2,
+                                            "[%d]=%"PRId64" ", j, keysizesHist[j]);
+            }
+            serverPanic("dbgAssertKeysizesHist: type=%d\nscanStr=%s\nkeysizes=%s\n",
+                        type, scanStr, keysizesStr);
+        }
+    }
+}
+
 /* Lookup a kvobj for read or write operations, or return NULL if the it is not
  * found in the specified DB. This function implements the functionality of
  * lookupKeyRead(), lookupKeyWrite() and their ...WithFlags() variants.
@@ -376,7 +416,7 @@ kvobj *dbAddRDBLoad(redisDb *db, sds key, robj **valref, long long expire) {
  *   update of a value of an existing key (when false).
  * - The `link` is optional, can save lookup, if provided.
  */
-static void dbSetValue(redisDb *db, robj *key, robj **valref, int overwrite, dictEntryLink link) {
+static void dbSetValue(redisDb *db, robj *key, robj **valref, dictEntryLink link, int overwrite, int updateKeySizes) {
     robj *val = *valref;
     int slot = getKeySlot(key->ptr);
     if (!link) {
@@ -441,12 +481,14 @@ static void dbSetValue(redisDb *db, robj *key, robj **valref, int overwrite, dic
 
     /* Remove old key and add new key to KEYSIZES histogram */
     int64_t newlen = (int64_t) getObjectLength(kvNew);
-    /* Save one call if old and new are the same type */
-    if (oldtype == kvNew->type) {
-        updateKeysizesHist(db, slot, oldtype, oldlen, newlen);
-    } else {
-        updateKeysizesHist(db, slot, oldtype, oldlen, -1);
-        updateKeysizesHist(db, slot, kvNew->type, -1, newlen);
+    if (updateKeySizes) {
+        /* Save one call if old and new are the same type */
+        if (oldtype == kvNew->type) {
+            updateKeysizesHist(db, slot, oldtype, oldlen, newlen);
+        } else {
+            updateKeysizesHist(db, slot, oldtype, oldlen, -1);
+            updateKeysizesHist(db, slot, kvNew->type, -1, newlen);
+        }
     }
 
     if (server.lazyfree_lazy_server_del) {
@@ -459,8 +501,8 @@ static void dbSetValue(redisDb *db, robj *key, robj **valref, int overwrite, dic
 
 /* Replace an existing key with a new value, we just replace value and don't
  * emit any events */
-void dbReplaceValue(redisDb *db, robj *key, robj **valref) {
-    dbSetValue(db, key, valref, 0, NULL);
+void dbReplaceValue(redisDb *db, robj *key, robj **valref, int updateKeySizes) {
+    dbSetValue(db, key, valref, NULL, 0, updateKeySizes);
 }
 
 /* Replace an existing key with a new value (don't emit any events)
@@ -468,7 +510,7 @@ void dbReplaceValue(redisDb *db, robj *key, robj **valref) {
  * parameter 'link' is optional. If provided, saves lookup.
  */
 void dbReplaceValueWithLink(redisDb *db, robj *key, robj **val, dictEntryLink link) {
-    dbSetValue(db, key, val, 0, link);
+    dbSetValue(db, key, val, link, 0, 1);
 }
 
 /* High level Set operation. This function can be used in order to set
@@ -515,7 +557,7 @@ void setKeyByLink(client *c, redisDb *db, robj *key, robj **valref, int flags, d
 
     if (exists) {
         /* Update the value of an existing key */
-        dbSetValue(db, key, valref, 1, *link);
+        dbSetValue(db, key, valref, *link, 1, 1);
         if ((-1 != kvobjGetExpire(*valref)) && (!(flags & SETKEY_KEEPTTL)))
             removeExpire(db,key);
     } else {
diff --git a/src/debug.c b/src/debug.c
index 7f49ab3c3ae..43b6cf13af6 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -412,6 +412,8 @@ void debugCommand(client *c) {
 "    Return hash table statistics of the specified Redis database.",
 "HTSTATS-KEY <key> [full]",
 "    Like HTSTATS but for the hash table stored at <key>'s value.",
+"KEYSIZES-HIST-ASSERT <0|1>",
+"    Enable/disable keysizes histogram assertion after each command.",
 "LOADAOF",
 "    Flush the AOF buffers on disk and reload the AOF in memory.",
 "REPLICATE <string>",
@@ -527,6 +529,12 @@ NULL
         addReply(c,shared.ok);
     } else if (!strcasecmp(c->argv[1]->ptr,"assert")) {
         serverAssertWithInfo(c,c->argv[0],1 == 2);
+    } else if (!strcasecmp(c->argv[1]->ptr,"KEYSIZES-HIST-ASSERT") && c->argc == 3) {
+        long long flag;
+        if (getLongLongFromObjectOrReply(c, c->argv[2], &flag, NULL) != C_OK)
+            return;
+        server.dbg_assert_keysizes = (flag != 0);
+        addReply(c, shared.ok);
     } else if (!strcasecmp(c->argv[1]->ptr,"log") && c->argc == 3) {
         serverLog(LL_WARNING, "DEBUG LOG: %s", (char*)c->argv[2]->ptr);
         addReply(c,shared.ok);
diff --git a/src/hyperloglog.c b/src/hyperloglog.c
index f873897d884..13fb3d8139e 100644
--- a/src/hyperloglog.c
+++ b/src/hyperloglog.c
@@ -1795,10 +1795,12 @@ void pfdebugCommand(client *c) {
         if (c->argc != 3) goto arityerr;
 
         if (hdr->encoding == HLL_SPARSE) {
+            uint64_t oldlen = (uint64_t) stringObjectLen(o);
             if (hllSparseToDense(o) == C_ERR) {
                 addReplyError(c,invalid_hll_err);
                 return;
             }
+            updateKeysizesHist(c->db, getKeySlot(c->argv[2]->ptr), OBJ_STRING, oldlen, stringObjectLen(o));
             server.dirty++; /* Force propagation on encoding change. */
         }
 
diff --git a/src/server.c b/src/server.c
index cbdd8964de3..03b37555c95 100644
--- a/src/server.c
+++ b/src/server.c
@@ -2172,6 +2172,7 @@ void initServerConfig(void) {
     server.configfile = NULL;
     server.executable = NULL;
     server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
+    server.dbg_assert_keysizes = 0; /* Disabled by default */
     server.bindaddr_count = CONFIG_DEFAULT_BINDADDR_COUNT;
     for (j = 0; j < CONFIG_DEFAULT_BINDADDR_COUNT; j++)
         server.bindaddr[j] = zstrdup(default_bindaddr[j]);
@@ -3935,7 +3936,6 @@ void rejectCommandFormat(client *c, const char *fmt, ...) {
 
 /* This is called after a command in call, we can do some maintenance job in it. */
 void afterCommand(client *c) {
-    UNUSED(c);
     /* Should be done before trackingHandlePendingKeyInvalidations so that we
      * reply to client before invalidating cache (makes more sense) */
     postExecutionUnitOperations();
@@ -3947,6 +3947,10 @@ void afterCommand(client *c) {
      * So the messages are not interleaved with transaction response. */
     if (!server.execution_nesting)
         listJoin(c->reply, server.pending_push_messages);
+
+    /* Assert keysizes histogram if enabled */
+    if (unlikely(server.dbg_assert_keysizes))
+        dbgAssertKeysizesHist(c->db);
 }
 
 /* Check if c->cmd exists, fills `err` with details in case it doesn't.
diff --git a/src/server.h b/src/server.h
index 70064bdd801..0a9d7260af4 100644
--- a/src/server.h
+++ b/src/server.h
@@ -2277,6 +2277,7 @@ struct redisServer {
     int reply_buffer_resizing_enabled; /* Is reply buffer resizing enabled (1 by default) */
     /* Local environment */
     char *locale_collate;
+    int dbg_assert_keysizes;       /* Assert keysizes histogram after each command */
 };
 
 /* we use 6 so that all getKeyResult fits a cacheline */
@@ -3583,6 +3584,7 @@ int setModuleNumericConfig(ModuleConfig *config, long long val, const char **err
 
 /* db.c -- Keyspace access API */
 void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, int64_t newLen);
+void dbgAssertKeysizesHist(redisDb *db);
 int removeExpire(redisDb *db, robj *key);
 void deleteExpiredKeyAndPropagate(redisDb *db, robj *keyobj);
 void deleteEvictedKeyAndPropagate(redisDb *db, robj *keyobj, long long *key_mem_freed);
@@ -3617,7 +3619,7 @@ static inline kvobj *dictGetKV(const dictEntry *de) {return (kvobj *) dictGetKey
 kvobj *dbAdd(redisDb *db, robj *key, robj **valref);
 kvobj *dbAddByLink(redisDb *db, robj *key, robj **valref, dictEntryLink *link);
 kvobj *dbAddRDBLoad(redisDb *db, sds key, robj **valref, long long expire);
-void dbReplaceValue(redisDb *db, robj *key, kvobj **ioKeyVal);
+void dbReplaceValue(redisDb *db, robj *key, kvobj **ioKeyVal, int updateKeySizes);
 void dbReplaceValueWithLink(redisDb *db, robj *key, robj **val, dictEntryLink link);
 
 #define SETKEY_KEEPTTL 1
diff --git a/src/t_set.c b/src/t_set.c
index 0272a5a2306..133167f3c0a 100644
--- a/src/t_set.c
+++ b/src/t_set.c
@@ -944,8 +944,12 @@ void spopWithCountCommand(client *c) {
         }
         setTypeReleaseIterator(si);
 
-        /* Assign the new set as the key value (Also update KEYSIZES histogram) */
-        dbReplaceValue(c->db, c->argv[1], &newset);
+        /* Update key size histogram "explicitly" and not indirectly by dbReplaceValue()
+         * since function dbReplaceValue() assumes the entire set is being replaced, 
+         * but here we're building the new set from the existing one. As a result, 
+         * the size of the old set has already changed by the time we reach this point. */
+        updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_SET, size, size-count);
+        dbReplaceValue(c->db, c->argv[1], &newset, 0);
     }
 
     /* Replicate/AOF the remaining elements as an SREM operation */
diff --git a/src/t_string.c b/src/t_string.c
index 7f747e18bee..64444c7781e 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -466,7 +466,8 @@ void setrangeCommand(client *c) {
         if (checkStringLength(c,offset,value_len) != C_OK)
             return;
 
-        robj *o = createObject(OBJ_STRING,sdsnewlen(NULL, offset+value_len));
+        newLen = offset+value_len;
+        robj *o = createObject(OBJ_STRING,sdsnewlen(NULL, newLen));
         kv = dbAddByLink(c->db, c->argv[1], &o, &link);
     } else {
         /* Key exists, check type */
@@ -486,6 +487,9 @@ void setrangeCommand(client *c) {
 
         /* Create a copy when the object is shared or encoded. */
         kv = dbUnshareStringValueByLink(c->db, c->argv[1], kv, link);
+
+        newLen = max(oldLen, (int64_t) (offset + value_len));
+        updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_STRING, oldLen, newLen);            
     }
 
     if (value_len > 0) {
@@ -497,8 +501,6 @@ void setrangeCommand(client *c) {
         server.dirty++;
     }
 
-    newLen = sdslen(kv->ptr);
-    updateKeysizesHist(c->db,getKeySlot(c->argv[1]->ptr),OBJ_STRING,oldLen,newLen);
     addReplyLongLong(c,newLen);
 }
 
diff --git a/tests/unit/info-keysizes.tcl b/tests/unit/info-keysizes.tcl
index 720412c984d..d926bbbef34 100644
--- a/tests/unit/info-keysizes.tcl
+++ b/tests/unit/info-keysizes.tcl
@@ -308,6 +308,8 @@ proc test_all_keysizes { {replMode 0} } {
         run_cmd_verify_hist {$server SADD s2 1} {db0_SET:1=1}
         run_cmd_verify_hist {$server SMOVE s2 s4 1} {db0_SET:1=1}
         run_cmd_verify_hist {$server SREM s4 1} {}
+        run_cmd_verify_hist {$server SADD s2 1 2 3 4 5 6 7 8} {db0_SET:8=1}
+        run_cmd_verify_hist {$server SPOP s2 7} {db0_SET:1=1}
         # SDIFFSTORE
         run_cmd_verify_hist {$server flushall} {}
         run_cmd_verify_hist {$server SADD s1 1 2 3 4 5 6 7 8} {db0_SET:8=1}
@@ -423,6 +425,8 @@ proc test_all_keysizes { {replMode 0} } {
         run_cmd_verify_hist {$server FLUSHALL} {}
         run_cmd_verify_hist {$server SET s2 1234567890} {db0_STR:8=1}
         run_cmd_verify_hist {$server SETRANGE s2 10 123456} {db0_STR:16=1}
+        run_cmd_verify_hist {$server FLUSHALL} {}
+        run_cmd_verify_hist {$server SETRANGE k 200000 v} {db0_STR:128K=1}
         # MSET, MSETNX
         run_cmd_verify_hist {$server FLUSHALL} {}
         run_cmd_verify_hist {$server MSET s3 1 s4 2 s5 3} {db0_STR:1=3}
@@ -471,6 +475,16 @@ proc test_all_keysizes { {replMode 0} } {
         run_cmd_verify_hist {} {__EVAL_DB_HIST__ 0} 1
     } {} {cluster:skip}
     
+    start_server {tags {"cluster:skip" "external:skip" "needs:debug"}} {
+        test "KEYSIZES - Test DEBUG KEYSIZES-HIST-ASSERT command" {
+        # Test based on debug command rather than __EVAL_DB_HIST__
+            r DEBUG KEYSIZES-HIST-ASSERT 1
+            r FLUSHALL
+            createComplexDataset r 100
+            createComplexDataset r 100 {useexpire usehexpire}
+        }
+    }
+    
     foreach type {listpackex hashtable} {
         # Test different implementations of hash tables and listpacks
         if {$type eq "hashtable"} {
@@ -719,7 +733,6 @@ proc test_all_keysizes { {replMode 0} } {
 }
 
 start_server {} {
-    # Test KEYSIZES on a single server
     r select 0
     test_all_keysizes 0
     # Start another server to test replication of KEYSIZES


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-11T07:25:35Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-11T07:41:02Z
user: login: sundb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T02:24:26Z
user: login: moticless
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T07:11:52Z
user: login: sundb
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T04:49:18Z
user: login: sundb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T04:51:49Z
user: login: moticless
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T08:50:12Z
user: login: sundb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T09:43:15Z
user: login: moticless
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T13:36:14Z
user: login: ShooterIT
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T14:23:28Z
user: login: moticless
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T18:27:14Z
user: login: sundb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T02:23:06Z
user: login: sundb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T12:11:13Z
user: login: sundb
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-19T13:58:45Z

Review comments:
diff_hunk: @@ -123,6 +123,45 @@
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
+    /* Scan DB and build expected histogram by scanning all keys */
+    int64_t scanHist[MAX_KEYSIZES_TYPES][MAX_KEYSIZES_BINS] = {0};
+    dictEntry *de;
+    kvstoreIterator *kvs_it = kvstoreIteratorInit(db->keys);
+    while ((de = kvstoreIteratorNext(kvs_it)) != NULL) {
+        robj *o = dictGetVal(de);
+        if (o->type < OBJ_TYPE_BASIC_MAX) {
+            int64_t len = getObjectLength(o);
+            scanHist[o->type][(len == 0) ? 0 : log2ceil(len) + 1]++;
+        }
+    }
+    kvstoreIteratorRelease(kvs_it);
+    for (int type = 0; type < OBJ_TYPE_BASIC_MAX; type++) {
+        volatile int64_t *keysizesHist = kvstoreGetMetadata(db->keys)->keysizes_hist[type];
+        for (int i = 0; i < MAX_KEYSIZES_BINS; i++) {
+            if (scanHist[type][i] != keysizesHist[i]) {
+                /* print scanStr vs. expected histograms for debugging */
+                char scanStr[500], keysizesStr[500];
+                int scanLen = 0, keysizesLen = 0;
+                for (int j = 0; j < MAX_KEYSIZES_BINS; j++) {
+                    if (scanHist[type][j])
+                        scanLen += snprintf(scanStr + scanLen, sizeof(scanStr) - scanLen,
path: src/db.c
user: login: github-advanced-security[bot]
body: ## Potentially overflowing call to snprintf

The [size argument](1) of this snprintf call is derived from its return value, which may exceed the size of the buffer and overflow.

[Show more details](https://github.com/redis/redis/security/code-scanning/52)
created_at: 2025-05-11T07:25:35Z
author_association: NONE
diff_hunk: @@ -123,6 +123,45 @@
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
+    /* Scan DB and build expected histogram by scanning all keys */
+    int64_t scanHist[MAX_KEYSIZES_TYPES][MAX_KEYSIZES_BINS] = {0};
+    dictEntry *de;
+    kvstoreIterator *kvs_it = kvstoreIteratorInit(db->keys);
+    while ((de = kvstoreIteratorNext(kvs_it)) != NULL) {
+        robj *o = dictGetVal(de);
+        if (o->type < OBJ_TYPE_BASIC_MAX) {
+            int64_t len = getObjectLength(o);
+            scanHist[o->type][(len == 0) ? 0 : log2ceil(len) + 1]++;
+        }
+    }
+    kvstoreIteratorRelease(kvs_it);
+    for (int type = 0; type < OBJ_TYPE_BASIC_MAX; type++) {
+        volatile int64_t *keysizesHist = kvstoreGetMetadata(db->keys)->keysizes_hist[type];
+        for (int i = 0; i < MAX_KEYSIZES_BINS; i++) {
+            if (scanHist[type][i] != keysizesHist[i]) {
+                /* print scanStr vs. expected histograms for debugging */
+                char scanStr[500], keysizesStr[500];
+                int scanLen = 0, keysizesLen = 0;
+                for (int j = 0; j < MAX_KEYSIZES_BINS; j++) {
+                    if (scanHist[type][j])
+                        scanLen += snprintf(scanStr + scanLen, sizeof(scanStr) - scanLen,
+                                            "[%d]=%ld ", j, scanHist[type][j]);
+                    if (keysizesHist[j])
+                        keysizesLen += snprintf(keysizesStr + keysizesLen, sizeof(keysizesStr) - keysizesLen,
path: src/db.c
user: login: github-advanced-security[bot]
body: ## Potentially overflowing call to snprintf

The [size argument](1) of this snprintf call is derived from its return value, which may exceed the size of the buffer and overflow.

[Show more details](https://github.com/redis/redis/security/code-scanning/53)
created_at: 2025-05-11T07:25:35Z
author_association: NONE
diff_hunk: @@ -123,6 +123,45 @@
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
+    /* Scan DB and build expected histogram by scanning all keys */
+    int64_t scanHist[MAX_KEYSIZES_TYPES][MAX_KEYSIZES_BINS] = {0};
+    dictEntry *de;
+    kvstoreIterator *kvs_it = kvstoreIteratorInit(db->keys);
+    while ((de = kvstoreIteratorNext(kvs_it)) != NULL) {
+        robj *o = dictGetVal(de);
+        if (o->type < OBJ_TYPE_BASIC_MAX) {
+            int64_t len = getObjectLength(o);
+            scanHist[o->type][(len == 0) ? 0 : log2ceil(len) + 1]++;
+        }
+    }
+    kvstoreIteratorRelease(kvs_it);
+    for (int type = 0; type < OBJ_TYPE_BASIC_MAX; type++) {
+        volatile int64_t *keysizesHist = kvstoreGetMetadata(db->keys)->keysizes_hist[type];
+        for (int i = 0; i < MAX_KEYSIZES_BINS; i++) {
+            if (scanHist[type][i] != keysizesHist[i]) {
+                /* print scanStr vs. expected histograms for debugging */
+                char scanStr[500], keysizesStr[500];
+                int scanLen = 0, keysizesLen = 0;
+                for (int j = 0; j < MAX_KEYSIZES_BINS; j++) {
+                    if (scanHist[type][j])
+                        scanLen += snprintf(scanStr + scanLen, sizeof(scanStr) - scanLen,
path: src/db.c
user: login: github-advanced-security[bot]
body: ## Potentially overflowing call to snprintf

The [size argument](1) of this snprintf call is derived from its return value, which may exceed the size of the buffer and overflow.

[Show more details](https://github.com/redis/redis/security/code-scanning/54)
created_at: 2025-05-11T07:41:01Z
author_association: NONE
diff_hunk: @@ -123,6 +123,45 @@
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
+    /* Scan DB and build expected histogram by scanning all keys */
+    int64_t scanHist[MAX_KEYSIZES_TYPES][MAX_KEYSIZES_BINS] = {0};
+    dictEntry *de;
+    kvstoreIterator *kvs_it = kvstoreIteratorInit(db->keys);
+    while ((de = kvstoreIteratorNext(kvs_it)) != NULL) {
+        robj *o = dictGetVal(de);
+        if (o->type < OBJ_TYPE_BASIC_MAX) {
+            int64_t len = getObjectLength(o);
+            scanHist[o->type][(len == 0) ? 0 : log2ceil(len) + 1]++;
+        }
+    }
+    kvstoreIteratorRelease(kvs_it);
+    for (int type = 0; type < OBJ_TYPE_BASIC_MAX; type++) {
+        volatile int64_t *keysizesHist = kvstoreGetMetadata(db->keys)->keysizes_hist[type];
+        for (int i = 0; i < MAX_KEYSIZES_BINS; i++) {
+            if (scanHist[type][i] != keysizesHist[i]) {
+                /* print scanStr vs. expected histograms for debugging */
+                char scanStr[500], keysizesStr[500];
+                int scanLen = 0, keysizesLen = 0;
+                for (int j = 0; j < MAX_KEYSIZES_BINS; j++) {
+                    if (scanHist[type][j])
+                        scanLen += snprintf(scanStr + scanLen, sizeof(scanStr) - scanLen,
+                                            "[%d]=%" PRId64 " ", j, scanHist[type][j]);
+                    if (keysizesHist[j])
+                        keysizesLen += snprintf(keysizesStr + keysizesLen, sizeof(keysizesStr) - keysizesLen,
path: src/db.c
user: login: github-advanced-security[bot]
body: ## Potentially overflowing call to snprintf

The [size argument](1) of this snprintf call is derived from its return value, which may exceed the size of the buffer and overflow.

[Show more details](https://github.com/redis/redis/security/code-scanning/55)
created_at: 2025-05-11T07:41:01Z
author_association: NONE
diff_hunk: @@ -39,7 +39,7 @@ typedef enum {
 static inline keyStatus expireIfNeededWithSlot(redisDb *db, robj *key, int flags, const int keySlot);
 keyStatus expireIfNeeded(redisDb *db, robj *key, int flags);
 int keyIsExpired(redisDb *db, robj *key);
-static void dbSetValue(redisDb *db, robj *key, robj *val, int overwrite, dictEntry *de);
+static void dbSetValue(redisDb *db, robj *key, robj *val, dictEntry *de, int overwrite, int updateKeySizes);
path: src/db.c
user: login: sundb
body: Why change the order of the parameters(de and overwrite)?
created_at: 2025-05-12T01:27:07Z
author_association: COLLABORATOR
diff_hunk: @@ -947,7 +947,8 @@ void spopWithCountCommand(client *c) {
         setTypeReleaseIterator(si);
 
         /* Assign the new set as the key value (Also update KEYSIZES histogram) */
-        dbReplaceValue(c->db,c->argv[1],newset);
+        updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_SET, size, size-count);
+        dbReplaceValue(c->db,c->argv[1],newset, 0);
path: src/t_set.c
user: login: sundb
body: please explain why `dbReplaceValue()` can't udpate the keysize when the number of elements to return is very large.
created_at: 2025-05-12T02:21:20Z
author_association: COLLABORATOR
diff_hunk: @@ -39,7 +39,7 @@ typedef enum {
 static inline keyStatus expireIfNeededWithSlot(redisDb *db, robj *key, int flags, const int keySlot);
 keyStatus expireIfNeeded(redisDb *db, robj *key, int flags);
 int keyIsExpired(redisDb *db, robj *key);
-static void dbSetValue(redisDb *db, robj *key, robj *val, int overwrite, dictEntry *de);
+static void dbSetValue(redisDb *db, robj *key, robj *val, dictEntry *de, int overwrite, int updateKeySizes);
path: src/db.c
user: login: moticless
body: I guess it makes sense (to me) to concentrate the flags close to each other
created_at: 2025-05-12T07:11:51Z
author_association: COLLABORATOR
diff_hunk: @@ -122,6 +122,46 @@ void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, in
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
path: src/db.c
user: login: sundb
body: Since it hasn't been used, I tend not to keep it. Can't we simply add it locally?
created_at: 2025-05-14T04:51:47Z
author_association: COLLABORATOR
diff_hunk: @@ -122,6 +122,46 @@ void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, in
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
path: src/db.c
user: login: moticless
body: This feature is quite fragile, so we should provide some way to debug it. I initially thought about adding an optional compilation flag, but that might introduce too much complexity. Another idea was to enable it through a debug command. In the end, I just added this function with a brief description. WDYT?
created_at: 2025-05-14T08:50:12Z
author_association: COLLABORATOR
diff_hunk: @@ -122,6 +122,46 @@ void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, in
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
path: src/db.c
user: login: sundb
body: feels more appropriate to add a debug command.
created_at: 2025-05-14T09:20:31Z
author_association: COLLABORATOR
diff_hunk: @@ -944,8 +944,12 @@ void spopWithCountCommand(client *c) {
         }
         setTypeReleaseIterator(si);
 
-        /* Assign the new set as the key value (Also update KEYSIZES histogram) */
-        dbReplaceValue(c->db, c->argv[1], &newset);
+        /* Update key size histogram "explicitly" and not indirectly by dbReplaceValue()
+         * since function dbReplaceValue() assumes the entire set is being replaced, 
+         * but here we're building the new set from the existing one. As a result, 
+         * the size of the old set has already changed by the time we reach this point. */        
path: src/t_set.c
user: login: sundb
body: ```suggestion
         * the size of the old set has already changed by the time we reach this point. */
```
created_at: 2025-05-14T09:20:42Z
author_association: COLLABORATOR
diff_hunk: @@ -486,6 +487,9 @@ void setrangeCommand(client *c) {
 
         /* Create a copy when the object is shared or encoded. */
         kv = dbUnshareStringValueByLink(c->db, c->argv[1], kv, link);
+
+        newLen = max(oldLen, (int64_t) (offset + value_len));
+        updateKeysizesHist(c->db, getKeySlot(c->argv[1]->ptr), OBJ_STRING, oldLen, newLen);
path: src/t_string.c
user: login: sundb
body: maybe we should explain here why we call updateKeysizesHist() in advance, or add a comment to dbAddByLink() to indicate that the length has been updated there.
created_at: 2025-05-14T09:43:13Z
author_association: COLLABORATOR
diff_hunk: @@ -122,6 +122,46 @@ void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, in
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
+void dbgAssertKeysizesHist(redisDb *db) {
path: src/db.c
user: login: moticless
body: pls consider my last commit. thanks.
created_at: 2025-05-18T13:36:14Z
author_association: COLLABORATOR
diff_hunk: @@ -2172,6 +2172,7 @@ void initServerConfig(void) {
     server.configfile = NULL;
     server.executable = NULL;
     server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
+    server.dbgAssertKeysize = 0; /* Disabled by default */
path: src/server.c
user: login: ShooterIT
body: what is `dbgAssertKeysize`?
We use the underscore naming convention, so why switch to camelCase here? As far as I know, camelCase is typically reserved for global variables.
created_at: 2025-05-18T14:23:28Z
author_association: MEMBER
diff_hunk: @@ -2172,6 +2172,7 @@ void initServerConfig(void) {
     server.configfile = NULL;
     server.executable = NULL;
     server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
+    server.dbgAssertKeysize = 0; /* Disabled by default */
path: src/server.c
user: login: moticless
body: modified to `dbg_assert_keysizes`. Thanks.
created_at: 2025-05-18T18:27:14Z
author_association: COLLABORATOR
diff_hunk: @@ -122,6 +122,46 @@ void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, in
     }
 }
 
+/* Assert keysizes histogram (For debugging only)
+ *
+ * To catch commands that corrupt the histogram, you can call it
+ * right after the call to "c->cmd->proc(c);" at server.c */
path: src/db.c
user: login: sundb
body: outdated?
created_at: 2025-05-19T02:21:34Z
author_association: COLLABORATOR
diff_hunk: @@ -471,6 +475,16 @@ proc test_all_keysizes { {replMode 0} } {
         run_cmd_verify_hist {} {__EVAL_DB_HIST__ 0} 1
     } {} {cluster:skip}
     
+    test "KEYSIZES - Test DEBUG KEYSIZES-HIST-ASSERT command" {
+        # Test based on debug command rather than __EVAL_DB_HIST__
+        start_server {} {
+            r DEBUG KEYSIZES-HIST-ASSERT 1
+            r FLUSHALL
+            createComplexDataset r 100
+            createComplexDataset r 100 {useexpire usehexpire}            
path: tests/unit/info-keysizes.tcl
user: login: sundb
body: ```suggestion
            createComplexDataset r 100 {useexpire usehexpire}
            r DEBUG KEYSIZES-HIST-ASSERT 0
```
created_at: 2025-05-19T02:22:37Z
author_association: COLLABORATOR
diff_hunk: @@ -471,6 +475,16 @@ proc test_all_keysizes { {replMode 0} } {
         run_cmd_verify_hist {} {__EVAL_DB_HIST__ 0} 1
     } {} {cluster:skip}
     
+    test "KEYSIZES - Test DEBUG KEYSIZES-HIST-ASSERT command" {
+        # Test based on debug command rather than __EVAL_DB_HIST__
+        start_server {} {
+            r DEBUG KEYSIZES-HIST-ASSERT 1
+            r FLUSHALL
+            createComplexDataset r 100
+            createComplexDataset r 100 {useexpire usehexpire}            
path: tests/unit/info-keysizes.tcl
user: login: sundb
body: @moticless you missed `r DEBUG KEYSIZES-HIST-ASSERT 0`, otherwise, this flag will affect other tests when running external test.
created_at: 2025-05-19T12:11:13Z
author_association: COLLABORATOR

PR comments:
user: login: gentcys
created_at: 2025-05-14T12:13:23Z
body: A new function can be added, instead of adding parameter to  `dbReplaceValue`:

```c
void dbReplaceValueSkipKeysizesUpdate(redisDb *db, robj *key, robj **valref) {
    dbSetValue(db, key, valref, NULL, 0, 0);
}
```

Then we can call it along with `updateKeysizesHist`.

Further more, regarding to `dbSetValue` function, it's common practice to use a "skip" parameter (`int skipKeysizesUpdate`), this makes it clear that the updating operation is performed unless explicitly skipped, because the default behavior is the most common case.
author_association: CONTRIBUTOR
user: login: moticless
created_at: 2025-05-18T11:41:32Z
body: @gentcys , I think it could be interpreted either way. When given two options, I usually lean toward the lazier one :). (Next time please link the comment to the code. Thank you)
author_association: COLLABORATOR
user: login: snyk-io[bot]
created_at: 2025-05-18T13:36:00Z
body: ![](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### :tada: **Snyk checks have passed. No issues have been found so far.**

:white_check_mark: **security/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/githubredis/pr-checks/fecd5872-4417-43ec-b1fd-37dc19d39e21?refs=437afadbcd9c82c186c10d3e4f00fb1c3bb9fa3c&source=prChecksComment)

:white_check_mark: **license/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/githubredis/pr-checks/fecd5872-4417-43ec-b1fd-37dc19d39e21/license?refs=437afadbcd9c82c186c10d3e4f00fb1c3bb9fa3c&source=prChecksComment)



author_association: NONE
user: login: oranagra
created_at: 2025-08-14T06:24:07Z
body: should this be backported to 8.0
author_association: MEMBER