Skip to content

Commit 028f541

Browse files
yperbasissudeepdino008
authored andcommitted
p2p: better handling of RLP errors in wit (#19569)
Fixes https://github.com/ethereum-bounty/erigon/issues/1
1 parent f01f94d commit 028f541

File tree

3 files changed

+118
-1
lines changed

3 files changed

+118
-1
lines changed

p2p/peer.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,17 @@ type Peer struct {
129129

130130
// NewPeer returns a peer for testing purposes.
131131
func NewPeer(id enode.ID, pubkey [64]byte, name string, caps []Cap, metricsEnabled bool) *Peer {
132+
return NewPeerWithProtocols(id, pubkey, name, caps, nil, metricsEnabled)
133+
}
134+
135+
// NewPeerWithProtocols returns a peer for testing purposes with the given
136+
// protocols registered in its running map. Caps and protocols must match
137+
// for a protocol to appear as running.
138+
func NewPeerWithProtocols(id enode.ID, pubkey [64]byte, name string, caps []Cap, protocols []Protocol, metricsEnabled bool) *Peer {
132139
pipe, _ := net.Pipe()
133140
node := enode.SignNull(new(enr.Record), id)
134141
conn := &conn{fd: pipe, transport: nil, node: node, caps: caps, name: name}
135-
peer := newPeer(log.Root(), conn, nil, pubkey, metricsEnabled)
142+
peer := newPeer(log.Root(), conn, protocols, pubkey, metricsEnabled)
136143
close(peer.closed) // ensures Disconnect doesn't block
137144
return peer
138145
}

p2p/sentry/sentry_grpc_server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ func runWitPeer(
655655
var query wit.NewWitnessPacket
656656
if err := rlp.DecodeBytes(b, &query); err != nil {
657657
logger.Error("decoding NewWitnessMsg: %w, data: %x", err, b)
658+
return p2p.NewPeerError(p2p.PeerErrorInvalidMessage, p2p.DiscSubprotocolError, err, "decoding NewWitnessMsg")
658659
}
659660

660661
peerInfo.AddKnownWitness(query.Witness.Header().Hash())
@@ -674,6 +675,7 @@ func runWitPeer(
674675
var query wit.NewWitnessHashesPacket
675676
if err := rlp.DecodeBytes(b, &query); err != nil {
676677
logger.Error("decoding NewWitnessHashesMsg: %w, data: %x", err, b)
678+
return p2p.NewPeerError(p2p.PeerErrorInvalidMessage, p2p.DiscSubprotocolError, err, "decoding NewWitnessHashesMsg")
677679
}
678680

679681
for _, hash := range query.Hashes {

p2p/sentry/sentry_grpc_server_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/erigontech/erigon/p2p/enode"
3232
"github.com/erigontech/erigon/p2p/forkid"
3333
"github.com/erigontech/erigon/p2p/protocols/eth"
34+
"github.com/erigontech/erigon/p2p/protocols/wit"
3435
)
3536

3637
// Handles RLP encoding/decoding for p2p.Msg
@@ -705,3 +706,110 @@ func TestSentryServerImpl_SetStatusInitPanic(t *testing.T) {
705706
t.Fatalf("error expected")
706707
}
707708
}
709+
710+
// newTestPeerInfoWithEth creates a PeerInfo backed by a *p2p.Peer that has
711+
// the eth protocol in its running map (so WaitForEth won't fail) and marks
712+
// the eth handshake as already completed.
713+
func newTestPeerInfoWithEth(t *testing.T) (*PeerInfo, [64]byte) {
714+
t.Helper()
715+
var pubkey [64]byte
716+
pubkey[0] = 0x01
717+
id := enode.ID{}
718+
copy(id[:], pubkey[:])
719+
720+
caps := []p2p.Cap{
721+
{Name: eth.ProtocolName, Version: direct.ETH68},
722+
}
723+
protocols := []p2p.Protocol{
724+
{Name: eth.ProtocolName, Version: direct.ETH68, Length: eth.ProtocolLengths[direct.ETH68]},
725+
}
726+
peer := p2p.NewPeerWithProtocols(id, pubkey, "test-peer", caps, protocols, false)
727+
728+
rw := NewRLPReadWriter()
729+
t.Cleanup(rw.Close)
730+
731+
pi := NewPeerInfo(peer, rw)
732+
// Mark eth handshake as done so WaitForEth returns immediately.
733+
pi.SetEthProtocol(direct.ETH68)
734+
735+
return pi, pubkey
736+
}
737+
738+
// TestRunWitPeer_MalformedNewWitnessMsg verifies that a malformed
739+
// NewWitnessMsg causes a PeerError (peer disconnect) rather than a
740+
// nil-pointer panic. This is the regression test for the DoS
741+
// vulnerability where a missing 'continue' after RLP decode failure
742+
// led to query.Witness.Header().Hash() panicking on a nil Witness.
743+
func TestRunWitPeer_MalformedNewWitnessMsg(t *testing.T) {
744+
t.Parallel()
745+
746+
peerInfo, peerID := newTestPeerInfoWithEth(t)
747+
748+
rw := NewRLPReadWriter()
749+
t.Cleanup(rw.Close)
750+
751+
logger := log.Root()
752+
753+
send := func(msgId sentryproto.MessageId, peerID [64]byte, b []byte) {}
754+
hasSubscribers := func(msgId sentryproto.MessageId) bool { return true }
755+
getWitnessRequest := func(hash common.Hash, peerID [64]byte) bool { return false }
756+
757+
// Feed a NewWitnessMsg with garbage RLP payload.
758+
garbage := []byte{0xff, 0xfe, 0xfd}
759+
rw.readCh <- p2p.Msg{
760+
Code: wit.NewWitnessMsg,
761+
Size: uint32(len(garbage)),
762+
Payload: io.NopCloser(bytes.NewReader(garbage)),
763+
}
764+
765+
errCh := make(chan *p2p.PeerError, 1)
766+
go func() {
767+
errCh <- runWitPeer(t.Context(), peerID, rw, peerInfo, send, hasSubscribers, getWitnessRequest, logger)
768+
}()
769+
770+
select {
771+
case peerErr := <-errCh:
772+
require.NotNil(t, peerErr, "expected a PeerError for malformed message")
773+
assert.Equal(t, p2p.PeerErrorInvalidMessage, peerErr.Code)
774+
case <-time.After(5 * time.Second):
775+
t.Fatal("runWitPeer did not return within timeout")
776+
}
777+
}
778+
779+
// TestRunWitPeer_MalformedNewWitnessHashesMsg verifies the same
780+
// protection for NewWitnessHashesMsg.
781+
func TestRunWitPeer_MalformedNewWitnessHashesMsg(t *testing.T) {
782+
t.Parallel()
783+
784+
peerInfo, peerID := newTestPeerInfoWithEth(t)
785+
786+
rw := NewRLPReadWriter()
787+
t.Cleanup(rw.Close)
788+
789+
logger := log.Root()
790+
791+
send := func(msgId sentryproto.MessageId, peerID [64]byte, b []byte) {}
792+
hasSubscribers := func(msgId sentryproto.MessageId) bool { return true }
793+
getWitnessRequest := func(hash common.Hash, peerID [64]byte) bool { return false }
794+
795+
// Feed a NewWitnessHashesMsg with garbage RLP payload.
796+
garbage := []byte{0xff, 0xfe, 0xfd}
797+
rw.readCh <- p2p.Msg{
798+
Code: wit.NewWitnessHashesMsg,
799+
Size: uint32(len(garbage)),
800+
Payload: io.NopCloser(bytes.NewReader(garbage)),
801+
}
802+
803+
errCh := make(chan *p2p.PeerError, 1)
804+
go func() {
805+
errCh <- runWitPeer(t.Context(), peerID, rw, peerInfo, send, hasSubscribers, getWitnessRequest, logger)
806+
}()
807+
808+
select {
809+
case peerErr := <-errCh:
810+
require.NotNil(t, peerErr, "expected a PeerError for malformed message")
811+
assert.Equal(t, p2p.PeerErrorInvalidMessage, peerErr.Code)
812+
case <-time.After(5 * time.Second):
813+
t.Fatal("runWitPeer did not return within timeout")
814+
}
815+
}

0 commit comments

Comments
 (0)