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()