wire: separate protocol message limit from serialization bound
What changed, and why it matters
This commit fixes a design mix-up in btcd's Bitcoin message handling. It restores the general 32 MB serialization safety limit and adds a separate ~4 MB limit specifically for peer-to-peer network messages. It also adds a missing size check for the newer v2 transport message reader. The change is defensive: it prevents very large messages from being accepted over the network while keeping internal database/RPC serialization limits consistent with Bitcoin Core.
Review and merge. The change is a defensive hardening patch that aligns btcd with Bitcoin Core's design. Operators should upgrade to ensure v2 transport messages are subject to the same size limits as v1 messages.
Security signals we found
Restores 32 MB serialization bound to avoid overly restrictive deserialization limits that could affect disk/RPC paths
Adds separate ~4 MB network message limit matching Bitcoin Core's MAX_PROTOCOL_MESSAGE_LENGTH
Adds missing overall message size enforcement in ReadV2MessageN v2 transport reader
Improves error typing in ReadV2MessageN (messageError instead of fmt.Errorf)
Evidence from the diff
The patch reverts MaxMessagePayload to 32 MiB and introduces MaxProtocolMessageLength (~4 MB). MaxMessagePayload is again used as a generic serialization bound for deriving per-message limits (maxTxInPerMessage, maxTxOutPerMessage, variable-length string limits). MaxProtocolMessageLength is enforced in WriteMessageN, WriteMessageWithEncodingN, ReadMessageWithEncodingN, and newly in ReadV2MessageN, which previously lacked an overall message length check. Error messages in ReadV2MessageN are also converted to typed messageError values.
Changed components
wire/message.gowire/message_test.goInspect captured patch +38 / −16
diff --git a/wire/message.go b/wire/message.go
index 074ac41..e4c0cda 100644
--- a/wire/message.go
+++ b/wire/message.go
@@ -23,8 +23,16 @@ const MessageHeaderSize = 24
const CommandSize = 12
// MaxMessagePayload is the maximum bytes a message can be regardless of other
-// individual limits imposed by messages themselves.
-const MaxMessagePayload = (1024 * 1024 * 4) // 4MB
+// individual limits imposed by messages themselves. This is used as a
+// serialization bound for all contexts (disk, RPC, network, etc.).
+const MaxMessagePayload = (1024 * 1024 * 32) // 32MiB
+
+// MaxProtocolMessageLength is the maximum length of an incoming/outgoing p2p
+// protocol message. This is separate from MaxMessagePayload which is used as a
+// general serialization bound. No current valid p2p message exceeds 4MB.
+// This mirrors Bitcoin Core's MAX_PROTOCOL_MESSAGE_LENGTH introduced in
+// bitcoin/bitcoin#5843.
+const MaxProtocolMessageLength = (4 * 1000 * 1000) // ~4MB
// Commands used in bitcoin message headers which describe the type of message.
const (
@@ -398,11 +406,11 @@ func WriteV2MessageN(w io.Writer, msg Message, pver uint32,
payload := bw.Bytes()
lenp := len(payload)
- // Enforce maximum overall message payload.
- if lenp > MaxMessagePayload {
+ // Enforce maximum protocol message payload.
+ if lenp > MaxProtocolMessageLength {
str := fmt.Sprintf("message payload is too large - encoded "+
"%d bytes, but maximum message payload is %d bytes",
- lenp, MaxMessagePayload)
+ lenp, MaxProtocolMessageLength)
return totalBytes, messageError("WriteMessage", str)
}
@@ -453,11 +461,11 @@ func WriteMessageWithEncodingN(w io.Writer, msg Message, pver uint32,
payload := bw.Bytes()
lenp := len(payload)
- // Enforce maximum overall message payload.
- if lenp > MaxMessagePayload {
+ // Enforce maximum protocol message payload.
+ if lenp > MaxProtocolMessageLength {
str := fmt.Sprintf("message payload is too large - encoded "+
"%d bytes, but maximum message payload is %d bytes",
- lenp, MaxMessagePayload)
+ lenp, MaxProtocolMessageLength)
return totalBytes, messageError("WriteMessage", str)
}
@@ -506,7 +514,8 @@ func ReadV2MessageN(plaintext []byte, pver uint32, enc MessageEncoding) (
Message, []byte, error) {
if len(plaintext) == 0 {
- return nil, nil, fmt.Errorf("invalid plaintext length")
+ return nil, nil, messageError("ReadV2MessageN",
+ "invalid plaintext length")
}
var msgCmd string
@@ -515,7 +524,8 @@ func ReadV2MessageN(plaintext []byte, pver uint32, enc MessageEncoding) (
// message this is.
if plaintext[0] == 0x00 {
if len(plaintext) < CommandSize+1 {
- return nil, nil, fmt.Errorf("invalid plaintext length")
+ return nil, nil, messageError("ReadV2MessageN",
+ "invalid plaintext length")
}
// Slice off the first 0x00 and the trailing 0x00 bytes.
@@ -537,9 +547,21 @@ func ReadV2MessageN(plaintext []byte, pver uint32, enc MessageEncoding) (
return nil, nil, err
}
+ // Enforce maximum protocol message payload.
+ if len(plaintext) > MaxProtocolMessageLength {
+ str := fmt.Sprintf("message payload is too large - "+
+ "%d bytes, but max message payload is %d bytes",
+ len(plaintext), MaxProtocolMessageLength)
+ return nil, nil, messageError("ReadV2MessageN", str)
+ }
+
+ // Check for maximum length based on the message type.
mpl := msg.MaxPayloadLength(pver)
if len(plaintext) > int(mpl) {
- return nil, nil, fmt.Errorf("payload exceeds max length")
+ str := fmt.Sprintf("payload exceeds max length - "+
+ "%d bytes, but max payload size for messages of "+
+ "type [%v] is %v.", len(plaintext), msgCmd, mpl)
+ return nil, nil, messageError("ReadV2MessageN", str)
}
buf := bytes.NewBuffer(plaintext)
@@ -599,11 +621,11 @@ func readMessageWithEncodingNInternal(r io.Reader, pver uint32,
hdr *messageHeader, btcnet BitcoinNet, enc MessageEncoding,
totalBytes int) (int, Message, []byte, error) {
- // Enforce maximum message payload.
- if hdr.length > MaxMessagePayload {
+ // Enforce maximum protocol message payload.
+ if hdr.length > MaxProtocolMessageLength {
str := fmt.Sprintf("message payload is too large - header "+
"indicates %d bytes, but max message payload is %d "+
- "bytes.", hdr.length, MaxMessagePayload)
+ "bytes.", hdr.length, MaxProtocolMessageLength)
return totalBytes, nil, nil, messageError("ReadMessage", str)
}
diff --git a/wire/message_test.go b/wire/message_test.go
index a9c8389..ce1d5df 100644
--- a/wire/message_test.go
+++ b/wire/message_test.go
@@ -203,7 +203,7 @@ func TestReadMessageWireErrors(t *testing.T) {
// Wire encoded bytes for a message that exceeds max overall message
// length.
- mpl := uint32(MaxMessagePayload)
+ mpl := uint32(MaxProtocolMessageLength)
exceedMaxPayloadBytes := makeHeader(btcnet, "getaddr", mpl+1, 0)
// Wire encoded bytes for a command which is invalid utf-8.
@@ -392,7 +392,7 @@ func TestWriteMessageWireErrors(t *testing.T) {
encodeErrMsg := &fakeMessage{forceEncodeErr: true}
// Fake message that has payload which exceeds max overall message size.
- exceedOverallPayload := make([]byte, MaxMessagePayload+1)
+ exceedOverallPayload := make([]byte, MaxProtocolMessageLength+1)
exceedOverallPayloadErrMsg := &fakeMessage{payload: exceedOverallPayload}
// Fake message that has payload which exceeds max allowed per message.
Why this scored 47/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.