fix(zap): bound element counts before make() to stop unbounded-alloc DoS

A decoded uint32 element count fed make([]T,count) before the per-element loop consumed any bytes, so a ~9-byte message with count=0xFFFFFFFF could drive a ~100GB allocation (OOM; the 16MB message cap and handler recover() do not bound it). Add Reader.ReadCount(), which rejects any count exceeding Remaining(), and use it in the 9 collection decoders across vm.go, sender.go, warp.go.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-06-06 18:39:05 -07:00
co-authored by Hanzo Dev
parent 7c1b716181
commit ec11283b34
4 changed files with 25 additions and 9 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ func (m *SendRequestMsg) Encode(buf *Buffer) {
// Decode deserializes SendRequestMsg from the reader
func (m *SendRequestMsg) Decode(r *Reader) error {
count, err := r.ReadUint32()
count, err := r.ReadCount()
if err != nil {
return err
}
@@ -121,7 +121,7 @@ func (m *SendGossipMsg) Encode(buf *Buffer) {
// Decode deserializes SendGossipMsg from the reader
func (m *SendGossipMsg) Decode(r *Reader) error {
count, err := r.ReadUint32()
count, err := r.ReadCount()
if err != nil {
return err
}
+4 -4
View File
@@ -425,7 +425,7 @@ func (m *BatchedParseBlockRequest) Encode(buf *Buffer) {
// Decode deserializes BatchedParseBlockRequest from the reader
func (m *BatchedParseBlockRequest) Decode(r *Reader) error {
count, err := r.ReadUint32()
count, err := r.ReadCount()
if err != nil {
return err
}
@@ -453,7 +453,7 @@ func (m *BatchedParseBlockResponse) Encode(buf *Buffer) {
// Decode deserializes BatchedParseBlockResponse from the reader
func (m *BatchedParseBlockResponse) Decode(r *Reader) error {
count, err := r.ReadUint32()
count, err := r.ReadCount()
if err != nil {
return err
}
@@ -513,7 +513,7 @@ func (m *GetAncestorsResponse) Encode(buf *Buffer) {
// Decode deserializes GetAncestorsResponse from the reader
func (m *GetAncestorsResponse) Decode(r *Reader) error {
count, err := r.ReadUint32()
count, err := r.ReadCount()
if err != nil {
return err
}
@@ -821,7 +821,7 @@ func (m *CreateHandlersResponse) Encode(buf *Buffer) {
// Decode deserializes CreateHandlersResponse from the reader
func (m *CreateHandlersResponse) Decode(r *Reader) error {
count, err := r.ReadUint32()
count, err := r.ReadCount()
if err != nil {
return err
}
+3 -3
View File
@@ -106,7 +106,7 @@ func (r *WarpBatchSignRequest) Encode(buf *Buffer) {
// Decode reads the request from the reader
func (r *WarpBatchSignRequest) Decode(rd *Reader) error {
count, err := rd.ReadUint32()
count, err := rd.ReadCount()
if err != nil {
return err
}
@@ -139,7 +139,7 @@ func (r *WarpBatchSignResponse) Encode(buf *Buffer) {
// Decode reads the response from the reader
func (r *WarpBatchSignResponse) Decode(rd *Reader) error {
sigCount, err := rd.ReadUint32()
sigCount, err := rd.ReadCount()
if err != nil {
return err
}
@@ -150,7 +150,7 @@ func (r *WarpBatchSignResponse) Decode(rd *Reader) error {
return err
}
}
errCount, err := rd.ReadUint32()
errCount, err := rd.ReadCount()
if err != nil {
return err
}
+16
View File
@@ -304,6 +304,22 @@ func (r *Reader) Remaining() int {
return len(r.data) - r.offset
}
// ReadCount reads a uint32 element count and rejects any value larger than the
// number of unread bytes. Every element costs at least one byte on the wire, so
// a count exceeding Remaining() can never be satisfied by the payload. Rejecting
// it here stops a hostile count (e.g. 0xFFFFFFFF) from driving a huge
// make([]T, count) allocation before the per-element decode loop reaches EOF.
func (r *Reader) ReadCount() (uint32, error) {
n, err := r.ReadUint32()
if err != nil {
return 0, err
}
if uint64(n) > uint64(r.Remaining()) {
return 0, ErrInvalidMessage
}
return n, nil
}
// WriteMessage writes a complete ZAP message with header
func WriteMessage(w io.Writer, msgType MessageType, payload []byte) error {
if len(payload) > MaxMessageSize {