Skip to content
Open
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
5 changes: 3 additions & 2 deletions consensus/XDPoS/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ func DecodeBytesExtraFields(b []byte, val interface{}) error {
if len(b) == 0 {
return errors.New("extra field is 0 length")
}
// Prevent payload attack, limit the size of extra field to 20k bytes. Normal Extrafield payload is less than 7k bytes.
if len(b) > 20000 {
// Prevent payload attack, limit the size of extra field to 20k bytes.
// Normal Extrafield payload is less than 7k bytes.
if len(b) > 20*1024 {
return errors.New("extra field is too long")
}

Expand Down
65 changes: 65 additions & 0 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ func (h *Header) HashNoValidator() common.Hash {

var headerSize = common.StorageSize(reflect.TypeOf(Header{}).Size())

const maxHeaderByteFieldSize = 20 * 1024

func checkHeaderByteFieldSize(name string, size int) error {
if size <= maxHeaderByteFieldSize {
return nil
}
return fmt.Errorf("too large block %s: size %d exceeds limit %d", name, size, maxHeaderByteFieldSize)
}

// Size returns the approximate memory used by all internal contents. It is used
// to approximate and limit the memory consumption of various caches.
func (h *Header) Size() common.StorageSize {
Expand All @@ -163,6 +172,53 @@ func (h *Header) Size() common.StorageSize {
return headerSize + common.StorageSize(len(h.Extra)+len(h.Validators)+len(h.Validator)+len(h.Penalties)+(h.Difficulty.BitLen()+h.Number.BitLen()+baseFeeBits)/8)
}

// SanityCheck checks a few basic things -- these checks are way beyond what
// any 'sane' production values should hold, and can mainly be used to prevent
// that the unbounded fields are stuffed with junk data to add processing
// overhead
func (h *Header) SanityCheck() error {
if h.Number != nil && !h.Number.IsUint64() {
return fmt.Errorf("too large block number: bitlen %d", h.Number.BitLen())
}
if h.Difficulty != nil {
if diffLen := h.Difficulty.BitLen(); diffLen > 80 {
return fmt.Errorf("too large block difficulty: bitlen %d", diffLen)
}
}
for _, field := range []struct {
name string
size int
}{
{name: "extradata", size: len(h.Extra)},
{name: "validators", size: len(h.Validators)},
{name: "validator", size: len(h.Validator)},
{name: "penalties", size: len(h.Penalties)},
} {
if err := checkHeaderByteFieldSize(field.name, field.size); err != nil {
return err
}
}
if h.BaseFee != nil {
if bfLen := h.BaseFee.BitLen(); bfLen > 256 {
return fmt.Errorf("too large base fee: bitlen %d", bfLen)
}
}
return nil
}

// SanityCheckHeaders verifies that a list of headers contains only reasonable values.
func SanityCheckHeaders(headers []*Header) error {
for index, header := range headers {
if header == nil {
return fmt.Errorf("nil block header at index %d", index)
}
if err := header.SanityCheck(); err != nil {
return err
}
}
return nil
}

// EmptyBody returns true if there is no additional 'body' to complete the header
// that is: no transactions and no uncles.
func (h *Header) EmptyBody() bool {
Expand Down Expand Up @@ -425,6 +481,15 @@ func (b *Block) Size() uint64 {
return uint64(c)
}

// SanityCheck can be used to prevent that unbounded fields are
// stuffed with junk data to add processing overhead
func (b *Block) SanityCheck() error {
if err := b.header.SanityCheck(); err != nil {
return err
}
return SanityCheckHeaders(b.uncles)
}

type writeCounter uint64

func (c *writeCounter) Write(b []byte) (int, error) {
Expand Down
92 changes: 92 additions & 0 deletions core/types/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,98 @@ func TestUncleHash(t *testing.T) {
}
}

func TestHeaderSanityCheckExtraSize(t *testing.T) {
tests := []struct {
name string
extra []byte
wantErr bool
}{
{name: "limit", extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize)},
{name: "over limit", extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
header := &Header{
Difficulty: big.NewInt(1),
Number: big.NewInt(1),
Extra: test.extra,
}
err := header.SanityCheck()
if test.wantErr && err == nil {
t.Fatal("expected oversized extra data to fail sanity check")
}
if !test.wantErr && err != nil {
t.Fatalf("expected extra data at limit to pass sanity check: %v", err)
}
})
}
}

func TestHeaderSanityCheckXDPoSFieldSizes(t *testing.T) {
tests := []struct {
name string
set func(*Header)
}{
{
name: "validators",
set: func(header *Header) {
header.Validators = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1)
},
},
{
name: "validator",
set: func(header *Header) {
header.Validator = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1)
},
},
{
name: "penalties",
set: func(header *Header) {
header.Penalties = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
header := &Header{
Difficulty: big.NewInt(1),
Number: big.NewInt(1),
}
test.set(header)
if err := header.SanityCheck(); err == nil {
t.Fatalf("expected oversized %s data to fail sanity check", test.name)
}
})
}
}

func TestBlockSanityCheckUncleExtraSize(t *testing.T) {
block := NewBlockWithHeader(&Header{
Difficulty: big.NewInt(1),
Number: big.NewInt(1),
}).WithBody(Body{
Uncles: []*Header{{
Difficulty: big.NewInt(1),
Number: big.NewInt(1),
Extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1),
}},
})

if err := block.SanityCheck(); err == nil {
t.Fatal("expected oversized uncle extra data to fail sanity check")
}
}

func TestSanityCheckHeadersExtraSize(t *testing.T) {
headers := []*Header{{
Extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1),
}}

if err := SanityCheckHeaders(headers); err == nil {
t.Fatal("expected oversized header extra data to fail sanity check")
}
}

var benchBuffer = bytes.NewBuffer(make([]byte, 0, 32000))

func BenchmarkEncodeBlock(b *testing.B) {
Expand Down
6 changes: 3 additions & 3 deletions eth/fetcher/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) {
// Remove any pending fetches and decrement the DOS counters
if announce := f.fetching[hash]; announce != nil {
f.announces[announce.origin]--
if f.announces[announce.origin] == 0 {
if f.announces[announce.origin] <= 0 {
delete(f.announces, announce.origin)
}
delete(f.fetching, hash)
Expand All @@ -776,7 +776,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) {
// Remove any pending completion requests and decrement the DOS counters
for _, announce := range f.fetched[hash] {
f.announces[announce.origin]--
if f.announces[announce.origin] == 0 {
if f.announces[announce.origin] <= 0 {
delete(f.announces, announce.origin)
}
}
Expand All @@ -785,7 +785,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) {
// Remove any pending completions and decrement the DOS counters
if announce := f.completing[hash]; announce != nil {
f.announces[announce.origin]--
if f.announces[announce.origin] == 0 {
if f.announces[announce.origin] <= 0 {
delete(f.announces, announce.origin)
}
delete(f.completing, hash)
Expand Down
9 changes: 9 additions & 0 deletions eth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
if err := msg.Decode(&headers); err != nil {
return errResp(ErrDecode, "msg %v: %v", msg, err)
}
if err := types.SanityCheckHeaders(headers); err != nil {
return err
}
// If no headers were received, but we're expending a DAO fork check, maybe it's that
if len(headers) == 0 && p.forkDrop != nil {
// Possibly an empty reply to the fork header checks, sanity check TDs
Expand Down Expand Up @@ -577,6 +580,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
if err := msg.Decode(&request); err != nil {
return errResp(ErrDecode, "msg %v: %v", msg, err)
}
if err := request.sanityCheck(); err != nil {
return err
}
// Deliver them all to the downloader for queuing
trasactions := make([][]*types.Transaction, len(request))
uncles := make([][]*types.Header, len(request))
Expand Down Expand Up @@ -715,6 +721,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
if err := msg.Decode(&request); err != nil {
return errResp(ErrDecode, "%v: %v", msg, err)
}
if err := request.sanityCheck(); err != nil {
return err
}
request.Block.ReceivedAt = msg.ReceivedAt
request.Block.ReceivedFrom = p

Expand Down
27 changes: 27 additions & 0 deletions eth/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,20 @@ type newBlockData struct {
TD *big.Int
}

// sanityCheck verifies that the values are reasonable, as a DoS protection
func (request *newBlockData) sanityCheck() error {
if err := request.Block.SanityCheck(); err != nil {
return err
}
// TD is 34 bits at mainnet block 105048538
// TD is 32 bits at testnet block 84385810.
// If it becomes 100 million times larger, it will still fit within 100 bits.
if tdlen := request.TD.BitLen(); tdlen > 100 {
return fmt.Errorf("too large block TD: bitlen %d", tdlen)
}
return nil
}

// blockBody represents the data content of a single block.
type blockBody struct {
Transactions []*types.Transaction // Transactions contained within a block
Expand All @@ -216,3 +230,16 @@ type blockBody struct {

// blockBodiesData is the network packet for block content distribution.
type blockBodiesData []*blockBody

// sanityCheck verifies that the uncle headers are reasonable, as a DoS protection.
func (request blockBodiesData) sanityCheck() error {
for bodyIndex, body := range request {
if body == nil {
return fmt.Errorf("nil block body at index %d", bodyIndex)
}
if err := types.SanityCheckHeaders(body.Uncles); err != nil {
return err
}
}
return nil
}
23 changes: 23 additions & 0 deletions eth/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package eth

import (
"bytes"
"fmt"
"sync"
"testing"
Expand Down Expand Up @@ -180,6 +181,28 @@ func testSendTransactions(t *testing.T, protocol int) {
wg.Wait()
}

func TestBlockBodiesDataSanityCheckUncleExtraSize(t *testing.T) {
request := blockBodiesData{{
Uncles: []*types.Header{{
Extra: bytes.Repeat([]byte{0x01}, 20*1024+1),
}},
}}

if err := request.sanityCheck(); err == nil {
t.Fatal("expected oversized uncle extra data to fail sanity check")
}
}

func TestBlockHeadersDataSanityCheckExtraSize(t *testing.T) {
headers := []*types.Header{{
Extra: bytes.Repeat([]byte{0x01}, 20*1024+1),
}}

if err := types.SanityCheckHeaders(headers); err == nil {
t.Fatal("expected oversized header extra data to fail sanity check")
}
}

// Tests that the custom union field encoder and decoder works correctly.
func TestGetBlockHeadersDataEncodeDecode(t *testing.T) {
// Create a "random" hash for testing
Expand Down
Loading