-
Notifications
You must be signed in to change notification settings - Fork 0
fix: enforce alloc limit in bytes decode; perf: exact-capacity readNGrow growth (#63) #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v6
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -761,18 +761,46 @@ func (d *Decoder) readN(n int) ([]byte, error) { | |
| return d.buf, nil | ||
| } | ||
|
|
||
| // readNInto reads n bytes into b, growing it as needed, and honors the | ||
| // decoder's alloc limit: unless DisableAllocLimit is set, allocation for | ||
| // a declared length is chunked by bytesAllocLimit so a malicious header | ||
| // can't force a huge upfront allocation. | ||
| func (d *Decoder) readNInto(b []byte, n int) ([]byte, error) { | ||
| // Byte-slice reader: the declared length can be validated against the | ||
| // data actually present before allocating, so a single exact-size | ||
| // allocation is safe regardless of the alloc limit. (Unlike d.readN's | ||
| // zero-copy fast path, the result here is caller-owned and must be a | ||
| // copy, not a sub-slice of the input.) | ||
| if d.bsr.data != nil { | ||
| if remaining := len(d.bsr.data) - d.bsr.pos; n > remaining { | ||
| // Match io.ReadFull semantics: EOF when no data remains, | ||
| // ErrUnexpectedEOF on a partial read. | ||
| if remaining == 0 && n > 0 { | ||
| return b, io.EOF | ||
| } | ||
| return b, io.ErrUnexpectedEOF | ||
| } | ||
| return readN(d.r, b, n) | ||
| } | ||
| if d.flags&disableAllocLimitFlag != 0 { | ||
| return readN(d.r, b, n) | ||
| } | ||
| return readNGrow(d.r, b, n) | ||
| } | ||
|
|
||
| func readN(r io.Reader, b []byte, n int) ([]byte, error) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent panics or silent data corruption on 32-bit architectures when a large func readN(r io.Reader, b []byte, n int) ([]byte, error) {
if n < 0 {
return nil, fmt.Errorf("msgpack: invalid length %d", n)
} |
||
| if b == nil { | ||
| if n == 0 { | ||
| return make([]byte, 0), nil | ||
| } | ||
| b = make([]byte, 0, n) | ||
| } | ||
|
|
||
| if n > cap(b) { | ||
| b = append(b, make([]byte, n-len(b))...) | ||
| b = make([]byte, n) | ||
| } else if n <= cap(b) { | ||
| b = b[:n] | ||
| } else { | ||
| // ReadFull overwrites all of b, so allocate exactly n instead of | ||
| // growing with append (which would copy the old contents for | ||
| // nothing and overshoot the capacity). | ||
| b = make([]byte, n) | ||
| } | ||
|
|
||
| _, err := io.ReadFull(r, b) | ||
|
|
@@ -799,30 +827,33 @@ func readNGrow(r io.Reader, b []byte, n int) ([]byte, error) { | |
| _, err := io.ReadFull(r, b) | ||
| return b, err | ||
| } | ||
| b = b[:cap(b)] | ||
|
|
||
| var pos int | ||
| for { | ||
| alloc := min(n-len(b), bytesAllocLimit) | ||
| b = append(b, make([]byte, alloc)...) | ||
|
|
||
| _, err := io.ReadFull(r, b[pos:]) | ||
| if err != nil { | ||
| // n exceeds the current capacity. Fill the available capacity first, | ||
| // then grow to at most one bytesAllocLimit chunk or 2x the bytes | ||
| // already received ahead of the data — whichever is larger — capped | ||
| // at the exact target n. A malicious declared length can therefore | ||
| // never force allocation more than max(received, bytesAllocLimit) | ||
| // ahead of the data actually supplied (the same bound the previous | ||
| // chunked append provided), while exact sizing avoids append's | ||
| // capacity overshoot beyond n and reduces alloc+copy rounds. | ||
| b = b[:cap(b)] | ||
| pos := len(b) | ||
| if pos > 0 { | ||
| if _, err := io.ReadFull(r, b); err != nil { | ||
| return b, err | ||
| } | ||
| } | ||
| for pos < n { | ||
| newCap := min(max(2*pos, pos+bytesAllocLimit), n) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On 32-bit architectures, if newCap := pos + bytesAllocLimit
if newCap < pos || newCap < 0 {
newCap = n
}
if double := 2 * pos; double > newCap && double > 0 {
newCap = double
}
if newCap > n || newCap < 0 {
newCap = n
} |
||
| nb := make([]byte, newCap) | ||
| copy(nb, b[:pos]) | ||
| b = nb | ||
|
|
||
| if len(b) == n { | ||
| break | ||
| if _, err := io.ReadFull(r, b[pos:]); err != nil { | ||
| return b, err | ||
| } | ||
| pos = len(b) | ||
| pos = newCap | ||
| } | ||
|
|
||
| return b, nil | ||
| } | ||
|
|
||
| func min(a, b int) int { //nolint:unparam | ||
| if a <= b { | ||
| return a | ||
| } | ||
| return b | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -92,7 +92,7 @@ func (d *Decoder) bytes(c byte, b []byte) ([]byte, error) { | |||||||||||||||||||||||||
| if n == -1 { | ||||||||||||||||||||||||||
| return nil, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return readN(d.r, b, n) | ||||||||||||||||||||||||||
| return d.readNInto(b, n) | ||||||||||||||||||||||||||
|
Comment on lines
92
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On 32-bit architectures, a large
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func (d *Decoder) decodeStringTemp() (string, error) { | ||||||||||||||||||||||||||
|
|
@@ -139,7 +139,7 @@ func (d *Decoder) bytesPtr(c byte, ptr *[]byte) error { | |||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| *ptr, err = readN(d.r, *ptr, n) | ||||||||||||||||||||||||||
| *ptr, err = d.readNInto(*ptr, n) | ||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On 32-bit architectures, a large
uint32length (e.g.,>= 2^31) decoded frombin32orstr32headers can overflow when cast toint, resulting in a negative value forn. Ifnis negative, it will bypass theremainingcheck and cause a panic inmake([]byte, n)or slice bounds out of range. We should add a check forn < 0at the beginning ofreadNIntoto prevent this.