From b5f667c06bc3c6a72b1f0d38432e539df7329457 Mon Sep 17 00:00:00 2001 From: sadd amr3e Date: Thu, 16 Jul 2026 18:37:24 +0530 Subject: [PATCH] fix(command): reject odd-length flat replies in Z/KeyValue parsers (#3900) * fix(command): reject odd-length flat replies in Z/KeyValue parsers * ci: retrigger govulncheck, setup-go now resolves 1.26.x to go1.26.5 --- command.go | 9 ++++++ command_flat_reply_test.go | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 command_flat_reply_test.go diff --git a/command.go b/command.go index db62ffc2..b0095ffa 100644 --- a/command.go +++ b/command.go @@ -2030,6 +2030,9 @@ func (cmd *KeyValueSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl if array { cmd.val = make([]KeyValue, n) } else { + if n%2 != 0 { + return fmt.Errorf("redis: got %d elements in the key-value array, wanted a multiple of 2", n) + } cmd.val = make([]KeyValue, n/2) } @@ -4137,6 +4140,9 @@ func (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl if array { cmd.val = make([]Z, n) } else { + if n%2 != 0 { + return fmt.Errorf("redis: got %d elements in the sorted set array, wanted a multiple of 2", n) + } cmd.val = make([]Z, n/2) } @@ -6235,6 +6241,9 @@ func (cmd *ZSliceWithKeyCmd) readReply(rd *proto.Reader) (err error) { if array { cmd.val = make([]Z, n) } else { + if n%2 != 0 { + return fmt.Errorf("redis: got %d elements in the sorted set array, wanted a multiple of 2", n) + } cmd.val = make([]Z, n/2) } diff --git a/command_flat_reply_test.go b/command_flat_reply_test.go new file mode 100644 index 00000000..2c2ce55e --- /dev/null +++ b/command_flat_reply_test.go @@ -0,0 +1,62 @@ +package redis + +import ( + "context" + "strings" + "testing" + + "github.com/redis/go-redis/v9/internal/proto" +) + +// A RESP2 flat reply (a single array of alternating member/score or key/value +// elements) must have an even length. An odd length means the parser would read +// n/2 pairs and leave the trailing element on the wire, desyncing the pooled +// connection. These parsers must reject it, matching the guard the sibling flat +// parsers (VLINKS, VSIM) already have. +func TestFlatReplyOddLengthRejected(t *testing.T) { + t.Run("ZSliceCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*3\r\n$1\r\na\r\n$3\r\n1.5\r\n$1\r\nb\r\n")) + cmd := NewZSliceCmd(context.Background()) + if err := cmd.readReply(rd); err == nil { + t.Fatalf("odd-length flat reply accepted; readReply returned nil (val=%+v)", cmd.val) + } + }) + t.Run("KeyValueSliceCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*3\r\n$1\r\nk\r\n$1\r\nv\r\n$1\r\nx\r\n")) + cmd := NewKeyValueSliceCmd(context.Background()) + if err := cmd.readReply(rd); err == nil { + t.Fatalf("odd-length flat reply accepted; readReply returned nil (val=%+v)", cmd.val) + } + }) + t.Run("ZSliceWithKeyCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*2\r\n$3\r\nzst\r\n*3\r\n$1\r\na\r\n$3\r\n1.5\r\n$1\r\nb\r\n")) + cmd := NewZSliceWithKeyCmd(context.Background()) + if err := cmd.readReply(rd); err == nil { + t.Fatalf("odd-length flat reply accepted; readReply returned nil (val=%+v)", cmd.val) + } + }) +} + +// Even-length flat replies and the nested-array form must still parse cleanly. +func TestFlatReplyValidStillParses(t *testing.T) { + t.Run("flat", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*4\r\n$1\r\na\r\n$3\r\n1.5\r\n$1\r\nb\r\n$3\r\n2.5\r\n")) + cmd := NewZSliceCmd(context.Background()) + if err := cmd.readReply(rd); err != nil { + t.Fatalf("valid flat reply: %v", err) + } + if len(cmd.val) != 2 || cmd.val[0].Member != "a" || cmd.val[1].Member != "b" { + t.Fatalf("unexpected val: %+v", cmd.val) + } + }) + t.Run("nested", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*2\r\n*2\r\n$1\r\na\r\n$3\r\n1.5\r\n*2\r\n$1\r\nb\r\n$3\r\n2.5\r\n")) + cmd := NewZSliceCmd(context.Background()) + if err := cmd.readReply(rd); err != nil { + t.Fatalf("valid nested reply: %v", err) + } + if len(cmd.val) != 2 { + t.Fatalf("unexpected val: %+v", cmd.val) + } + }) +}