#38766 [BC-Insight] Nil Pointer Dereference Panics in encodePayload() of Blob Tx’s Encoding
Submitted on Jan 13th 2025 at 03:44:37 UTC by @CertiK for Attackathon | Ethereum Protocol
Report ID: #38766
Report Type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/ledgerwatch/erigon
Impacts:
(Specifications) A bug in specifications with no direct impact on client implementations
Description
Brief/Intro
EIP-4844
(https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md ) introduces a new transaction type, blob tx, which does not allow the To
address to be nil (i.e., it cannot be used to create contract).
In Erigon (https://github.com/erigontech/erigon/ ), the blob tx allows the To
address to be nil. In case that the blob tx includes a nil To
address, the tx encoding causes a nil pointer dereference panics due to a flaw in function encodePayload()
.
Vulnerability Details
Affected Codebase: https://github.com/erigontech/erigon/tree/v2.61.0
The blob tx is defined via embedding the DynamicFeeTransaction
, which allows the To address to be nil.
https://github.com/erigontech/erigon/blob/v2.61.0/core/types/blob_tx.go#L20C1-L24C2
type BlobTx struct {
DynamicFeeTransaction
MaxFeePerBlobGas *uint256.Int
BlobVersionedHashes []libcommon.Hash
}
https://github.com/erigontech/erigon/blob/v2.61.0/core/types/dynamic_fee_tx.go#L35
type DynamicFeeTransaction struct {
CommonTx
ChainID *uint256.Int
Tip *uint256.Int
FeeCap *uint256.Int
AccessList types2.AccessList
}
https://github.com/erigontech/erigon/blob/v2.61.0/core/types/legacy_tx.go#L34
type CommonTx struct {
TransactionMisc
Nonce uint64 // nonce of sender account
Gas uint64 // gas limit
To *libcommon.Address `rlp:"nil"` // nil means contract creation
Value *uint256.Int // wei amount
Data []byte // contract invocation input data
V, R, S uint256.Int // signature values
}
However, this violates the EIP-4844
(https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md )
The field to deviates slightly from the semantics with the exception that it MUST NOT be nil and therefore must always represent a 20-byte address. This means that blob transactions cannot have the form of a create transaction.
In case that the To
address is set to be nil in a blob tx, the encoding with two functions EncodeRLP()
and MarshalBinary()
calls the encodePayload()
to perform the encoding of blob tx:
https://github.com/erigontech/erigon/blob/v2.61.0/core/types/blob_tx.go#L243
func (stx *BlobTx) EncodeRLP(w io.Writer) error {
payloadSize, nonceLen, gasLen, accessListLen, blobHashesLen := stx.payloadSize()
// size of struct prefix and TxType
envelopeSize := 1 + rlp.ListPrefixLen(payloadSize) + payloadSize
b := newEncodingBuf()
defer pooledBuf.Put(b)
// envelope
if err := rlp.EncodeStringSizePrefix(envelopeSize, w, b[:]); err != nil {
return err
}
// encode TxType
b[0] = BlobTxType
if _, err := w.Write(b[:1]); err != nil {
return err
}
if err := stx.encodePayload(w, b[:], payloadSize, nonceLen, gasLen, accessListLen, blobHashesLen); err != nil {
return err
}
return nil
}
func (stx *BlobTx) MarshalBinary(w io.Writer) error {
payloadSize, nonceLen, gasLen, accessListLen, blobHashesLen := stx.payloadSize()
b := newEncodingBuf()
defer pooledBuf.Put(b)
// encode TxType
b[0] = BlobTxType
if _, err := w.Write(b[:1]); err != nil {
return err
}
if err := stx.encodePayload(w, b[:], payloadSize, nonceLen, gasLen, accessListLen, blobHashesLen); err != nil {
return err
}
return nil
}
https://github.com/erigontech/erigon/blob/v2.61.0/core/types/blob_tx.go#L167
func (stx *BlobTx) encodePayload(w io.Writer, b []byte, payloadSize, nonceLen, gasLen, accessListLen, blobHashesLen int) error {
// prefix
if err := rlp.EncodeStructSizePrefix(payloadSize, w, b); err != nil {
return err
}
// encode ChainID
if err := rlp.EncodeUint256(stx.ChainID, w, b); err != nil {
return err
}
// encode Nonce
if err := rlp.EncodeInt(stx.Nonce, w, b); err != nil {
return err
}
// encode MaxPriorityFeePerGas
if err := rlp.EncodeUint256(stx.Tip, w, b); err != nil {
return err
}
// encode MaxFeePerGas
if err := rlp.EncodeUint256(stx.FeeCap, w, b); err != nil {
return err
}
// encode Gas
if err := rlp.EncodeInt(stx.Gas, w, b); err != nil {
return err
}
// encode To
b[0] = 128 + 20
if _, err := w.Write(b[:1]); err != nil {
return err
}
if _, err := w.Write(stx.To[:]); err != nil {
return err
}
// encode Value
if err := rlp.EncodeUint256(stx.Value, w, b); err != nil {
return err
}
// encode Data
if err := rlp.EncodeString(stx.Data, w, b); err != nil {
return err
}
// prefix
if err := rlp.EncodeStructSizePrefix(accessListLen, w, b); err != nil {
return err
}
// encode AccessList
if err := encodeAccessList(stx.AccessList, w, b); err != nil {
return err
}
// encode MaxFeePerBlobGas
if err := rlp.EncodeUint256(stx.MaxFeePerBlobGas, w, b); err != nil {
return err
}
// prefix
if err := rlp.EncodeStructSizePrefix(blobHashesLen, w, b); err != nil {
return err
}
// encode BlobVersionedHashes
if err := encodeBlobVersionedHashes(stx.BlobVersionedHashes, w, b); err != nil {
return err
}
// encode V
if err := rlp.EncodeUint256(&stx.V, w, b); err != nil {
return err
}
// encode R
if err := rlp.EncodeUint256(&stx.R, w, b); err != nil {
return err
}
// encode S
if err := rlp.EncodeUint256(&stx.S, w, b); err != nil {
return err
}
return nil
}
At line 197, the stx.To.Bytes()
directly invokes the Bytes()
method without checking if the To
is nil or not. This overlook would lead to the nil pointer dereference panics.
The latest commit makes slight changes to the affected code, but it still cause nil pointer deference panics when writing nil To
address. (Demonstrated in the Proof of Concept)
https://github.com/erigontech/erigon/blob/main/core/types/blob_tx.go#L218
// encode To
b[0] = 128 + 20
if _, err := w.Write(b[:1]); err != nil {
return err
}
if _, err := w.Write(stx.To[:]); err != nil {
return err
}
Impact Details
Since the encoding of tx is used frequently in the codebase, through users, p2p or consensus layer, any invocation of the blob tx encoding with nil To
address would crash the Erigon node.
References
https://github.com/erigontech/erigon/tree/v2.61.0
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md
Proof of Concept
Proof of Concept
For simplicity, using the existing tests in file https://github.com/erigontech/erigon/blob/v2.61.0/core/types/transaction_test.go#L600 we create the following unit test to demonstrate the nil pointer dereference panics with MarshalBinary() of the blob tx.
Set the
To
address as nil:
func newRandBlobTx() *BlobTx {
stx := &BlobTx{DynamicFeeTransaction: DynamicFeeTransaction{
CommonTx: CommonTx{
Nonce: rand.Uint64(),
Gas: rand.Uint64(),
//To: randAddr(),
To: nil,
Value: uint256.NewInt(rand.Uint64()),
Data: randData(),
V: *uint256.NewInt(0),
R: *uint256.NewInt(rand.Uint64()),
S: *uint256.NewInt(rand.Uint64()),
},
ChainID: uint256.NewInt(rand.Uint64()),
Tip: uint256.NewInt(rand.Uint64()),
FeeCap: uint256.NewInt(rand.Uint64()),
AccessList: randAccessList(),
},
MaxFeePerBlobGas: uint256.NewInt(rand.Uint64()),
BlobVersionedHashes: randHashes(randIntInRange(1, 6)),
}
return stx
}
Run the following unit test in the same file:
func TestBlobTxEncodeDecode(t *testing.T) {
rand.Seed(time.Now().UnixNano())
populateBlobTxs()
for i := 0; i < N; i++ {
tx, err := encodeDecodeBinary(dummyBlobTxs[i])
if err != nil {
t.Fatal(err)
}
if err := assertEqual(dummyBlobTxs[i], tx); err != nil {
t.Fatal(err)
}
}
}
func encodeDecodeBinary(tx Transaction) (Transaction, error) {
var buf bytes.Buffer
var err error
if err = tx.MarshalBinary(&buf); err != nil {
return nil, fmt.Errorf("rlp encoding failed: %w", err)
}
var parsedTx Transaction
if parsedTx, err = UnmarshalTransactionFromBinary(buf.Bytes(), false /* blobTxnsAreWrappedWithBlobs */); err != nil {
return nil, fmt.Errorf("rlp decoding failed: %w", err)
}
return parsedTx, nil
}
The test result shows an nil pointer dereference panics occur due to the method invocation
To.Bytes()
:
=== RUN TestBlobTxEncodeDecode
--- FAIL: TestBlobTxEncodeDecode (0.01s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10232483e]
goroutine 25 [running]:
testing.tRunner.func1.2({0x1026be860, 0x102c99f30})
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1632 +0x230
testing.tRunner.func1()
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1635 +0x35e
panic({0x1026be860?, 0x102c99f30?})
/Users/***/go/pkg/mod/golang.org/[email protected]/src/runtime/panic.go:785 +0x132
github.com/erigontech/erigon/core/types.(*BlobTx).encodePayload(0xc0001c0500, {0x1027acba0, 0xc0005ae1e0}, {0xc0001267b0, 0x21, 0x21}, 0xc0005ae1e0?, 0x0?, 0x0?, 0x118, ...)
/Users/***/immunefi/erigon/core/types/blob_tx.go:197 +0x17e
github.com/erigontech/erigon/core/types.(*BlobTx).MarshalBinary(0xc0001c0500, {0x1027acba0, 0xc0005ae1e0})
/Users/***/immunefi/erigon/core/types/blob_tx.go:274 +0xde
github.com/erigontech/erigon/core/types.encodeDecodeBinary({0x1027bf260, 0xc0001c0500})
/Users/***/immunefi/erigon/core/types/transaction_test.go:591 +0x50
github.com/erigontech/erigon/core/types.TestBlobTxEncodeDecode(0xc000142680)
/Users/***/immunefi/erigon/core/types/transaction_test.go:871 +0xf0
testing.tRunner(0xc000142680, 0x1027a6508)
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1690 +0xf4
created by testing.(*T).Run in goroutine 1
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1743 +0x390
Process finished with the exit code 1
Nil Pointer dereference also occurs in the latest commit (https://github.com/erigontech/erigon/blob/main/core/types/blob_tx.go#L218 ) when writing nil
To
address:
=== RUN TestBlobTxEncodeDecode
--- FAIL: TestBlobTxEncodeDecode (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10c83485b]
goroutine 11 [running]:
testing.tRunner.func1.2({0x10cbce860, 0x10d1a9f30})
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1632 +0x230
testing.tRunner.func1()
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1635 +0x35e
panic({0x10cbce860?, 0x10d1a9f30?})
/Users/***/go/pkg/mod/golang.org/[email protected]/src/runtime/panic.go:785 +0x132
github.com/erigontech/erigon/core/types.(*BlobTx).encodePayload(0xc000256100, {0x10ccbcba0, 0xc00063ced0}, {0xc000126630, 0x21, 0x21}, 0xc00063ced0?, 0x0?, 0x0?, 0x188, ...)
/Users/***/immunefi/erigon/core/types/blob_tx.go:200 +0x17b
github.com/erigontech/erigon/core/types.(*BlobTx).MarshalBinary(0xc000256100, {0x10ccbcba0, 0xc00063ced0})
/Users/***/immunefi/erigon/core/types/blob_tx.go:274 +0xde
github.com/erigontech/erigon/core/types.encodeDecodeBinary({0x10cccf260, 0xc000256100})
/Users/***/immunefi/erigon/core/types/transaction_test.go:591 +0x50
github.com/erigontech/erigon/core/types.TestBlobTxEncodeDecode(0xc0001424e0)
/Users/***/immunefi/erigon/core/types/transaction_test.go:871 +0xf0
testing.tRunner(0xc0001424e0, 0x10ccb6508)
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1690 +0xf4
created by testing.(*T).Run in goroutine 1
/Users/***/go/pkg/mod/golang.org/[email protected]/src/testing/testing.go:1743 +0x390
Process finished with the exit code 1
Was this helpful?