From dc8f9d691390da7f3d00e8a5b089753fbe64c710 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2026 20:17:29 +0100 Subject: [PATCH 1/6] rlp: validate and cache element count in RawList This changes the RawList to ensure the count of items is always valid. Lists with invalid structure, i.e. ones where an element exceeds the size of the container, are now detected during decoding of the RawList. --- rlp/encode.go | 2 +- rlp/raw.go | 28 +++++++++++++++++++---- rlp/raw_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/rlp/encode.go b/rlp/encode.go index ba8959ee6f4e..9d04e6324aa3 100644 --- a/rlp/encode.go +++ b/rlp/encode.go @@ -122,7 +122,7 @@ func EncodeToRawList[T any](val []T) (RawList[T], error) { bytes := make([]byte, contentSize+9) offset := 9 - headsize(uint64(contentSize)) buf.copyTo(bytes[offset:]) - return RawList[T]{enc: bytes}, nil + return RawList[T]{enc: bytes, length: len(val)}, nil } type listhead struct { diff --git a/rlp/raw.go b/rlp/raw.go index 96ad0427c89b..798165621ef4 100644 --- a/rlp/raw.go +++ b/rlp/raw.go @@ -44,6 +44,9 @@ type RawList[T any] struct { // The implementation code mostly works with the Content method because it // returns something valid either way. enc []byte + + // len holds the number of items in the list. + length int } // Content returns the RLP-encoded data of the list. @@ -87,7 +90,14 @@ func (r *RawList[T]) DecodeRLP(s *Stream) error { if err := s.readFull(enc[9:]); err != nil { return err } - *r = RawList[T]{enc: enc} + n, err := CountValues(enc[9:]) + if err != nil { + if err == ErrValueTooLarge { + return ErrElemTooLarge + } + return err + } + *r = RawList[T]{enc: enc, length: n} return nil } @@ -105,8 +115,7 @@ func (r *RawList[T]) Items() ([]T, error) { // Len returns the number of items in the list. func (r *RawList[T]) Len() int { - len, _ := CountValues(r.Content()) - return len + return r.length } // Size returns the encoded size of the list. @@ -142,15 +151,26 @@ func (r *RawList[T]) Append(item T) error { end := prevEnd + eb.size() r.enc = slices.Grow(r.enc, eb.size())[:end] eb.copyTo(r.enc[prevEnd:end]) + r.length++ return nil } // AppendRaw adds an encoded item to the list. -func (r *RawList[T]) AppendRaw(b []byte) { +// The given byte slice must contain exactly one RLP value. +func (r *RawList[T]) AppendRaw(b []byte) error { + _, tagsize, contentsize, err := readKind(b) + if err != nil { + return err + } + if tagsize+contentsize != uint64(len(b)) { + return fmt.Errorf("rlp: input has trailing bytes in AppendRaw") + } if r.enc == nil { r.enc = make([]byte, 9) } r.enc = append(r.enc, b...) + r.length++ + return nil } // StringSize returns the encoded size of a string. diff --git a/rlp/raw_test.go b/rlp/raw_test.go index 9a4c68050c2c..104db58533bc 100644 --- a/rlp/raw_test.go +++ b/rlp/raw_test.go @@ -226,6 +226,66 @@ func TestRawListAppend(t *testing.T) { } } +func TestRawListAppendRaw(t *testing.T) { + var rl RawList[uint64] + + // Valid single-value appends. + if err := rl.AppendRaw(unhex("01")); err != nil { + t.Fatal("AppendRaw(01) failed:", err) + } + if err := rl.AppendRaw(unhex("820102")); err != nil { + t.Fatal("AppendRaw(820102) failed:", err) + } + if rl.Len() != 2 { + t.Fatalf("wrong Len %d after valid appends", rl.Len()) + } + + // Empty input. + if err := rl.AppendRaw(nil); err == nil { + t.Fatal("AppendRaw(nil) should fail") + } + + // Trailing bytes: two values concatenated. + if err := rl.AppendRaw(unhex("0102")); err == nil { + t.Fatal("AppendRaw(0102) should fail due to trailing bytes") + } + + // Truncated value: tag claims more data than present. + if err := rl.AppendRaw(unhex("8201")); err == nil { + t.Fatal("AppendRaw(8201) should fail due to truncated value") + } + + // Len should be unchanged after failed appends. + if rl.Len() != 2 { + t.Fatalf("wrong Len %d after invalid appends, want 2", rl.Len()) + } +} + +func TestRawListDecodeInvalid(t *testing.T) { + tests := []struct { + input string + err error + }{ + // Single item with non-canonical size (0x81 wrapping byte <= 0x7F). + {input: "C28142", err: ErrCanonSize}, + // Single item claiming more bytes than available in the list. + {input: "C484020202", err: ErrElemTooLarge}, + // Two items, second has non-canonical size. + {input: "C3018142", err: ErrCanonSize}, + // Two items, second claims more bytes than remain in the list. + {input: "C401830202", err: ErrElemTooLarge}, + // Item is a sub-list whose declared size exceeds available bytes. + {input: "C3C40102", err: ErrElemTooLarge}, + } + for _, test := range tests { + var rl RawList[RawValue] + err := DecodeBytes(unhex(test.input), &rl) + if !errors.Is(err, test.err) { + t.Errorf("input %s: error mismatch: got %v, want %v", test.input, err, test.err) + } + } +} + func TestCountValues(t *testing.T) { tests := []struct { input string // note: spaces in input are stripped by unhex From 3336912f3b8b0dad9d6bc9bf6be1ecc795c23225 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2026 20:20:45 +0100 Subject: [PATCH 2/6] rlp: remove comments in test --- rlp/raw_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/rlp/raw_test.go b/rlp/raw_test.go index 104db58533bc..03e298af8dea 100644 --- a/rlp/raw_test.go +++ b/rlp/raw_test.go @@ -229,7 +229,6 @@ func TestRawListAppend(t *testing.T) { func TestRawListAppendRaw(t *testing.T) { var rl RawList[uint64] - // Valid single-value appends. if err := rl.AppendRaw(unhex("01")); err != nil { t.Fatal("AppendRaw(01) failed:", err) } @@ -240,22 +239,15 @@ func TestRawListAppendRaw(t *testing.T) { t.Fatalf("wrong Len %d after valid appends", rl.Len()) } - // Empty input. if err := rl.AppendRaw(nil); err == nil { t.Fatal("AppendRaw(nil) should fail") } - - // Trailing bytes: two values concatenated. if err := rl.AppendRaw(unhex("0102")); err == nil { t.Fatal("AppendRaw(0102) should fail due to trailing bytes") } - - // Truncated value: tag claims more data than present. if err := rl.AppendRaw(unhex("8201")); err == nil { t.Fatal("AppendRaw(8201) should fail due to truncated value") } - - // Len should be unchanged after failed appends. if rl.Len() != 2 { t.Fatalf("wrong Len %d after invalid appends, want 2", rl.Len()) } From 7610cff62a667fbdb3023292b46753e58ef4c035 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2026 21:07:13 +0100 Subject: [PATCH 3/6] rlp: remove RawList.Empty --- rlp/raw.go | 5 ----- rlp/raw_test.go | 3 --- 2 files changed, 8 deletions(-) diff --git a/rlp/raw.go b/rlp/raw.go index 798165621ef4..811a8e5e5ade 100644 --- a/rlp/raw.go +++ b/rlp/raw.go @@ -123,11 +123,6 @@ func (r *RawList[T]) Size() uint64 { return ListSize(uint64(len(r.Content()))) } -// Empty returns true if the list contains no items. -func (r *RawList[T]) Empty() bool { - return len(r.Content()) == 0 -} - // ContentIterator returns an iterator over the content of the list. // Note the offsets returned by iterator.Offset are relative to the // Content bytes of the list. diff --git a/rlp/raw_test.go b/rlp/raw_test.go index 03e298af8dea..67e626ebf9b6 100644 --- a/rlp/raw_test.go +++ b/rlp/raw_test.go @@ -154,9 +154,6 @@ func TestRawListEmpty(t *testing.T) { if !bytes.Equal(b, unhex("C0")) { t.Fatalf("empty RawList has wrong encoding %x", b) } - if !rl.Empty() { - t.Fatal("list should be Empty") - } if rl.Len() != 0 { t.Fatalf("empty list has Len %d", rl.Len()) } From ab067839969dff1fd3676d02295dd9a91c692a37 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2026 21:11:06 +0100 Subject: [PATCH 4/6] rlp: remove Iterator.Count This was introduced together with RawList, but it's slightly unsafe since CountValues returns zero upon encountering an invalid item. This means the invariant documented on Count, that it provides an upper bound on the number of visited items, is not true. Since there are no callers of Count (yet), it's best to just remove it. --- rlp/iterator.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rlp/iterator.go b/rlp/iterator.go index f1f214c4bc60..d6151eabd731 100644 --- a/rlp/iterator.go +++ b/rlp/iterator.go @@ -64,15 +64,6 @@ func (it *Iterator) Next() bool { return true } -// Count returns the remaining number of items. -// Note this is O(n) and the result may be incorrect if the list data is invalid. -// The returned count is always an upper bound on the remaining items -// that will be visited by the iterator. -func (it *Iterator) Count() int { - count, _ := CountValues(it.data) - return count -} - // Value returns the current value. func (it *Iterator) Value() []byte { return it.next From 934538de8cbc9faf7ede1e9a94095ce70383080d Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2026 21:16:22 +0100 Subject: [PATCH 5/6] rlp: fix typo in comment --- rlp/raw.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlp/raw.go b/rlp/raw.go index 811a8e5e5ade..69946f6d224a 100644 --- a/rlp/raw.go +++ b/rlp/raw.go @@ -45,7 +45,7 @@ type RawList[T any] struct { // returns something valid either way. enc []byte - // len holds the number of items in the list. + // length holds the number of items in the list. length int } From ab057a97d65bf638ab2613c1f6bdefc443e15e58 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2026 21:27:59 +0100 Subject: [PATCH 6/6] rlp: remove it.Count in tests --- rlp/raw_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/rlp/raw_test.go b/rlp/raw_test.go index 67e626ebf9b6..49ac3dcc6272 100644 --- a/rlp/raw_test.go +++ b/rlp/raw_test.go @@ -68,9 +68,6 @@ func (test rawListTest[T]) run(t *testing.T) { // check iterator it := rl.ContentIterator() i := 0 - if count := it.Count(); count != test.length { - t.Fatalf("iterator has wrong Count %d, want %d", count, test.length) - } for it.Next() { var item T if err := DecodeBytes(it.Value(), &item); err != nil {