This is an automated email from the ASF dual-hosted git repository.

git-hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new f7ea9b2ac fix(rdb): reject malformed intset lengths (#3519)
f7ea9b2ac is described below

commit f7ea9b2ac8194789f4460c66dead5c180ec794a7
Author: hulk <[email protected]>
AuthorDate: Fri Jun 12 22:36:48 2026 +0800

    fix(rdb): reject malformed intset lengths (#3519)
    
    RESTORE loads intset contents from a length-prefixed RDB string. The
    parser checked IntSetHeaderSize + len * record_size using 32-bit
    arithmetic, so a large len could overflow the product and make a
    header-only intset appear correctly sized. It then entered the entry
    loop and read beyond the provided input.
    
    Before this patch, a RESTORE payload using int64 intset encoding and len
    0x20000000 timed out or terminated the server instead of returning a
    parse error. After this patch, the same payload returns "ERR invalid
    intset length," and the server continues to respond to PING.
    
    Validate the encoding before using it as a record size, compare the
    declared length with the remaining payload to ensure no overflow, and
    perform a bounds check before each record read.
    
    Assistant By GPT-5.5 HIGH
---
 src/storage/rdb/rdb_intset.cc             | 10 +++++++---
 tests/gocase/unit/restore/restore_test.go | 32 +++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 3 deletions(-)

diff --git a/src/storage/rdb/rdb_intset.cc b/src/storage/rdb/rdb_intset.cc
index 328e7a0d9..daf798a85 100644
--- a/src/storage/rdb/rdb_intset.cc
+++ b/src/storage/rdb/rdb_intset.cc
@@ -45,11 +45,12 @@ StatusOr<std::vector<std::string>> IntSet::Entries() {
   pos_ += sizeof(uint32_t);
   memrev32ifbe(&len);
 
-  uint32_t record_size = encoding;
-  if (record_size == 0) {
+  if (encoding != IntSetEncInt16 && encoding != IntSetEncInt32 && encoding != 
IntSetEncInt64) {
     return {Status::NotOK, "invalid intset encoding"};
   }
-  if (IntSetHeaderSize + len * record_size != input_.size()) {
+  uint32_t record_size = encoding;
+  if (len != (input_.size() - IntSetHeaderSize) / record_size ||
+      (input_.size() - IntSetHeaderSize) % record_size != 0) {
     return {Status::NotOK, "invalid intset length"};
   }
 
@@ -57,6 +58,7 @@ StatusOr<std::vector<std::string>> IntSet::Entries() {
   for (uint32_t i = 0; i < len; i++) {
     switch (encoding) {
       case IntSetEncInt16: {
+        GET_OR_RET(peekOk(sizeof(uint16_t)));
         uint16_t v = 0;
         memcpy(&v, input_.data() + pos_, sizeof(uint16_t));
         pos_ += sizeof(uint16_t);
@@ -65,6 +67,7 @@ StatusOr<std::vector<std::string>> IntSet::Entries() {
         break;
       }
       case IntSetEncInt32: {
+        GET_OR_RET(peekOk(sizeof(uint32_t)));
         uint32_t v = 0;
         memcpy(&v, input_.data() + pos_, sizeof(uint32_t));
         pos_ += sizeof(uint32_t);
@@ -73,6 +76,7 @@ StatusOr<std::vector<std::string>> IntSet::Entries() {
         break;
       }
       case IntSetEncInt64: {
+        GET_OR_RET(peekOk(sizeof(uint64_t)));
         uint64_t v = 0;
         memcpy(&v, input_.data() + pos_, sizeof(uint64_t));
         pos_ += sizeof(uint64_t);
diff --git a/tests/gocase/unit/restore/restore_test.go 
b/tests/gocase/unit/restore/restore_test.go
index e4c8bc8c8..d92cc39a0 100644
--- a/tests/gocase/unit/restore/restore_test.go
+++ b/tests/gocase/unit/restore/restore_test.go
@@ -21,6 +21,8 @@ package restore
 
 import (
        "context"
+       "encoding/binary"
+       "hash/crc64"
        "testing"
        "time"
 
@@ -248,6 +250,36 @@ func TestRestore_Set(t *testing.T) {
        })
 }
 
+func TestRestoreRejectsInvalidIntSetLength(t *testing.T) {
+       // Redis CRC64 reflected polynomial, used by the DUMP/RESTORE payload 
footer.
+       redisCRC64Table := crc64.MakeTable(0x95ac9329ac4bc9b5)
+       redisCRC64 := func(data []byte) uint64 {
+               return ^crc64.Update(^uint64(0), redisCRC64Table, data)
+       }
+
+       srv := util.StartServer(t, map[string]string{})
+       defer srv.Close()
+
+       ctx := context.Background()
+       rdb := srv.NewClient()
+       defer func() { require.NoError(t, rdb.Close()) }()
+
+       // RDBTypeSetIntSet with an 8-byte string payload: int64 encoding and a 
length
+       // large enough to overflow the old intset size check.
+       body := []byte{0x0b, 0x08}
+       body = binary.LittleEndian.AppendUint32(body, 8)
+       body = binary.LittleEndian.AppendUint32(body, 0x20000000)
+       // RDB version 11 followed by the Redis CRC64 checksum.
+       body = binary.LittleEndian.AppendUint16(body, 11)
+       value := string(binary.LittleEndian.AppendUint64(body, 
redisCRC64(body)))
+
+       restoreCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+       defer cancel()
+       require.ErrorContains(t, rdb.Restore(restoreCtx, util.RandString(32, 
64, util.Alpha), 0, value).Err(),
+               "ERR invalid intset length")
+       require.NoError(t, rdb.Ping(ctx).Err())
+}
+
 func TestRestoreWithTTL(t *testing.T) {
        srv := util.StartServer(t, map[string]string{})
        defer srv.Close()

Reply via email to