Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions encoder/cborlite/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cborlite_test

import (
"math"
"testing"

"github.com/NethermindEth/juno/encoder/cborlite"
"github.com/stretchr/testify/require"
)

type reader struct {
name string
read func(data []byte) (int, bool)
}

func readers() []reader {
return []reader{
{"Head", func(data []byte) (int, bool) {
_, _, consumed, ok := cborlite.Head(data)
return consumed, ok
}},
{"Skip", cborlite.Skip},
{"BytesNoCopy", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.BytesNoCopy(data)
return consumed, ok
}},
{"Bytes", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.Bytes(data)
return consumed, ok
}},
{"StringNoCopy", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.StringNoCopy(data)
return consumed, ok
}},
{"String", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.String(data)
return consumed, ok
}},
{"Uint64", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.Uint64(data)
return consumed, ok
}},
{"Int64", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.Int64(data)
return consumed, ok
}},
{"BigInt", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.BigInt(data)
return consumed, ok
}},
{"Bool", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.Bool(data)
return consumed, ok
}},
{"Tag", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.Tag(data)
return consumed, ok
}},
{"ArrayHeader", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.ArrayHeader(data)
return consumed, ok
}},
{"MapHeader", func(data []byte) (int, bool) {
_, consumed, ok := cborlite.MapHeader(data)
return consumed, ok
}},
{"ReadNull", cborlite.ReadNull},
}
}

func FuzzReadersStayInBounds(f *testing.F) {
f.Add([]byte{})
f.Add(head(uintMajor, 0))
f.Add([]byte{null})
f.Add([]byte{simpleFalse})
f.Add([]byte{simpleTrue})
f.Add(head(simpleMajor, 23)) // undefined, which is neither null nor a boolean
f.Add(head(simpleMajor, 32))
f.Add(head(uintMajor, 255))
f.Add(head(uintMajor, math.MaxUint64))
f.Add(cborBytes(0xaa, 0xbb, 0xcc))
f.Add(cborText("abc"))
f.Add(cborArray(head(uintMajor, 1), head(uintMajor, 2)))
f.Add(cborMap(cborText("k"), head(uintMajor, 1)))
f.Add(cborTagged(tagPositiveBignum, cborBytes(beyondUint64.Bytes()...)))
f.Add(cborArray(
cborArray(head(uintMajor, 1), head(uintMajor, 2)),
cborMap(head(uintMajor, 3), head(uintMajor, 4)),
))
f.Add([]byte{initialByte(arrayMajor, 31), 1, 0xff}) // Indefinite length.
f.Add([]byte{initialByte(arrayMajor, info1Byte), 255, 0x01}) // Too little bytes.

all := readers()

f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 1024 {
return
}

points := []int{0, 1, len(data) / 2, len(data)}

for _, r := range all {
for _, at := range points {
if at > len(data) {
continue
}

window := data[at:]
consumed, ok := r.read(window)
if !ok {
continue
}

require.Positivef(t, consumed,
"%s reported success without making progress on %d bytes",
r.name, len(window))
require.LessOrEqualf(t, consumed, len(window),
"%s reported consuming %d of %d bytes",
r.name, consumed, len(window))
}
}
})
}
98 changes: 98 additions & 0 deletions encoder/cborlite/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cborlite

import "encoding/binary"

const (
// MajorMask and InfoMask split a header byte into
// major type (top 3 bits) and additional info (low 5 bits).
MajorMask = 0b1110_0000
InfoMask = 0b0001_1111

// Major types. They define the object type.
// See https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1
UintMajor = 0 << 5
NegIntMajor = 1 << 5
BytesMajor = 2 << 5
StringMajor = 3 << 5
ArrayMajor = 4 << 5
MapMajor = 5 << 5
TagMajor = 6 << 5
SimpleMajor = 7 << 5

// bool values and nil pointers.
SimpleFalse = SimpleMajor | 20
SimpleTrue = SimpleMajor | 21
Null = SimpleMajor | 22

// Tag types to define Big Numbers
// https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3
TagPositiveBignum = 2
TagNegativeBignum = 3

// Additional info values that say how many bytes follow the argument.
Info1Byte = 24
Info2Byte = 25
Info4Byte = 26
Info8Byte = 27

// Always one byte.
headerSize = 1
)

// Head reads the header at the start of data.
// argument is a value for a scalar, the length for a string, or an item count for an array/map.
func Head(data []byte) (major byte, argument uint64, consumed int, ok bool) {
if len(data) == 0 {
return 0, 0, 0, false
}

header := data[0]
major = header & MajorMask
info := header & InfoMask

// The argument is so small it fits inside info.
if info < Info1Byte {
return major, uint64(info), headerSize, true
}

// info says the number of bytes that follow, max 8.
if info > Info8Byte {
return 0, 0, 0, false
}
infoByteSize := 1 << (info - Info1Byte)
if len(data) < headerSize+infoByteSize {
return 0, 0, 0, false
}

switch infoByteSize {
case 1:
argument = uint64(data[headerSize])
case 2:
argument = uint64(binary.BigEndian.Uint16(data[headerSize:]))
case 4:
argument = uint64(binary.BigEndian.Uint32(data[headerSize:]))
default:
argument = binary.BigEndian.Uint64(data[headerSize:])
}
return major, argument, headerSize + infoByteSize, true
Comment thread
RafaelGranza marked this conversation as resolved.
}

// ArrayHeader reads an array header and its element count.
func ArrayHeader(data []byte) (length, consumed int, ok bool) {
major, count, consumed, ok := Head(data)
// An element takes at least one byte, data must have space for it.
if !ok || major != ArrayMajor || count > uint64(len(data)-consumed) {
return 0, 0, false
}
return int(count), consumed, true
}
Comment thread
RafaelGranza marked this conversation as resolved.

// MapHeader reads a map header and its pair count.
func MapHeader(data []byte) (pairsCount, consumed int, ok bool) {
major, count, consumed, ok := Head(data)
// A pair takes at least two bytes, data must have space for it.
if !ok || major != MapMajor || count > uint64(len(data)-consumed)/2 {
return 0, 0, false
}
return int(count), consumed, true
}
167 changes: 167 additions & 0 deletions encoder/cborlite/headers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package cborlite_test

import (
"math"
"testing"

"github.com/NethermindEth/juno/encoder/cborlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHead(t *testing.T) {
tests := []struct {
name string
data []byte
major byte
argument uint64
consumed int
ok bool
}{
{
name: "argument inside the header byte",
data: head(uintMajor, 23),
major: uintMajor, argument: 23, consumed: 1, ok: true,
},
{
name: "one byte argument",
data: head(uintMajor, 255),
major: uintMajor, argument: 255, consumed: 2, ok: true,
},
{
name: "two byte argument",
data: head(uintMajor, 256),
major: uintMajor, argument: 256, consumed: 3, ok: true,
},
{
name: "four byte argument",
data: head(uintMajor, 65536),
major: uintMajor, argument: 65536, consumed: 5, ok: true,
},
{
name: "eight byte argument",
data: head(uintMajor, 1<<32),
major: uintMajor, argument: 1 << 32, consumed: 9, ok: true,
},
{
// The reader stops at the end of the header and never looks at what follows
name: "reads only the header, whatever trails it",
data: cborText("abc"),
major: stringMajor, argument: 3, consumed: 1, ok: true,
},
{
name: "major type is decoded independently of the argument",
data: []byte{initialByte(mapMajor, 1)}, major: mapMajor, argument: 1, consumed: 1, ok: true,
},
{name: "empty buffer", data: []byte{}},
{name: "one byte argument truncated", data: []byte{initialByte(uintMajor, info1Byte)}},
{name: "two byte argument truncated", data: []byte{initialByte(uintMajor, info2Byte), 0x01}},
{
name: "four byte argument truncated",
data: []byte{initialByte(uintMajor, info4Byte), 0x00, 0x01},
},
{
name: "eight byte argument truncated",
data: []byte{initialByte(uintMajor, info8Byte), 0, 0, 0, 1},
},
{name: "reserved additional info 28", data: headerFollowedBy(initialByte(uintMajor, 28), 16)},
{name: "reserved additional info 29", data: headerFollowedBy(initialByte(uintMajor, 29), 32)},
{name: "reserved additional info 30", data: headerFollowedBy(initialByte(uintMajor, 30), 64)},
{name: "indefinite length", data: headerFollowedBy(initialByte(uintMajor, 31), 128)},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
major, argument, consumed, ok := cborlite.Head(test.data)

require.Equal(t, test.ok, ok)
if !test.ok {
return
}
assert.Equal(t, test.major, major)
assert.Equal(t, test.argument, argument)
assert.Equal(t, test.consumed, consumed)
})
}
}

func TestArrayHeader(t *testing.T) {
tests := []struct {
name string
data []byte
length int
consumed int
ok bool
}{
{
name: "count and header width",
data: cborArray(head(uintMajor, 1), head(uintMajor, 2)),
length: 2, consumed: 1, ok: true,
},
{name: "empty array is zero", data: cborArray(), consumed: 1, ok: true},
// Null is a different item, not an array of no elements.
{name: "declines null", data: []byte{null}},
// Guards the allocation: every element needs at least one byte.
{
name: "rejects a count past the remaining bytes",
data: []byte{initialByte(arrayMajor, 5), 0x01, 0x02},
},
// The count is returned as an int, so this one is the case that would come back
// negative and make every length check downstream read backwards.
{name: "rejects a count of all ones", data: head(arrayMajor, math.MaxUint64)},
{
name: "rejects another major type",
data: cborMap(head(uintMajor, 1), head(uintMajor, 2)),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
length, consumed, ok := cborlite.ArrayHeader(test.data)

require.Equal(t, test.ok, ok)
if !test.ok {
return
}
assert.Equal(t, test.length, length)
assert.Equal(t, test.consumed, consumed)
})
}
}

func TestMapHeader(t *testing.T) {
tests := []struct {
name string
data []byte
pairs int
consumed int
ok bool
}{
{
name: "pair count and header width",
data: cborMap(head(uintMajor, 1), head(uintMajor, 2)),
pairs: 1, consumed: 1, ok: true,
},
{name: "empty map is zero", data: cborMap(), consumed: 1, ok: true},
{name: "declines null", data: []byte{null}},
// A pair takes two bytes at the very least, so this count cannot be honoured.
{name: "rejects a count past the remaining bytes", data: []byte{initialByte(mapMajor, 5), 0x01}},
{
name: "rejects an array",
data: cborArray(head(uintMajor, 1), head(uintMajor, 2)),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pairs, consumed, ok := cborlite.MapHeader(test.data)

require.Equal(t, test.ok, ok)
if !test.ok {
return
}
assert.Equal(t, test.pairs, pairs)
assert.Equal(t, test.consumed, consumed)
})
}
}
Loading
Loading