mirror of
https://github.com/luxfi/lattice.git
synced 2026-09-24 00:25:44 +00:00
security(buffer): bound ReadUintNSlice to prevent DoS via crafted Reader (#3)
ReadUint{16,32,64}Slice recurses on c[buffered:] without making progress
when the underlying Reader returns a partial Peek (buffered == 0) but no
error. An attacker-supplied wire prefix combined with a long destination
slice exhausts the goroutine stack on the destination chain.
Discovered by github.com/luxfi/warp/pulsar's FuzzPulseDeserialize
harness during the Mar-3 2026 PQ Consensus Architecture Freeze
(regression seed: testdata/fuzz/FuzzPulseDeserialize/ccdb090e0ca0007b).
Fix:
- Replace recursion with an iterative loop (O(1) stack)
- Detect zero-progress reads and return ErrZeroProgress
- Add Read{Uint16,Uint32,Uint64}SliceBounded length-prefix-aware
wrappers gated by MaxSliceLen() (default 1 MiB, configurable via
LUX_LATTICE_MAX_UINT64_SLICE_LEN env var)
- Regression tests for all three slice readers + happy path + bounded
rejection
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Lux Industries Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// DefaultMaxSliceLen is the default maximum slice length accepted by the
|
||||
// length-bounded slice readers (Read{Uint16,Uint32,Uint64}SliceBounded). It
|
||||
// is intentionally large enough to cover all in-tree lattice serialization
|
||||
// paths (a Poly with N=2^17 coefficients only needs 131072 entries) while
|
||||
// still rejecting attacker-controlled length prefixes that would force the
|
||||
// callee to allocate gigabytes or recurse without bound.
|
||||
//
|
||||
// The cap is a per-call ceiling on the destination slice length, NOT the
|
||||
// total bytes peeked from the reader. It exists to give downstream callers
|
||||
// (e.g. luxfi/warp) a predictable upper bound when they delegate parsing to
|
||||
// this package without the full lattigo Poly wire walker.
|
||||
const DefaultMaxSliceLen = 1 << 20 // 1,048,576 entries
|
||||
|
||||
// EnvMaxSliceLen overrides DefaultMaxSliceLen at process start when set to
|
||||
// a positive integer. Values <= 0 fall back to the default. Reading the
|
||||
// environment is done once per process; subsequent changes are ignored.
|
||||
const EnvMaxSliceLen = "LUX_LATTICE_MAX_UINT64_SLICE_LEN"
|
||||
|
||||
var (
|
||||
// ErrSliceTooLarge is returned by the length-bounded slice readers when
|
||||
// the requested destination length exceeds the configured cap.
|
||||
ErrSliceTooLarge = errors.New("buffer: slice length exceeds maximum")
|
||||
|
||||
// ErrZeroProgress is returned by ReadUint{16,32,64}Slice when the
|
||||
// underlying Reader returns a partial Peek that cannot decode at least
|
||||
// one element. Without this guard, the slice readers would recurse on
|
||||
// the unchanged tail (c[buffered:] with buffered == 0) and exhaust the
|
||||
// goroutine stack on an attacker-controlled wire prefix.
|
||||
//
|
||||
// Discovered via fuzzing in github.com/luxfi/warp/pulsar
|
||||
// (FuzzPulseDeserialize, regression seed ccdb090e0ca0007b, 2026-03-03).
|
||||
ErrZeroProgress = errors.New("buffer: zero-progress slice read (short input)")
|
||||
|
||||
maxSliceLen atomic.Int64
|
||||
)
|
||||
|
||||
func init() {
|
||||
maxSliceLen.Store(int64(DefaultMaxSliceLen))
|
||||
if v := os.Getenv(EnvMaxSliceLen); v != "" {
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 {
|
||||
maxSliceLen.Store(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaxSliceLen returns the currently configured per-call slice cap.
|
||||
func MaxSliceLen() int { return int(maxSliceLen.Load()) }
|
||||
|
||||
// SetMaxSliceLen overrides the configured cap. n <= 0 resets to default.
|
||||
// Intended for tests and for callers that need a tighter bound than the
|
||||
// default 1 MiB.
|
||||
func SetMaxSliceLen(n int) {
|
||||
if n <= 0 {
|
||||
maxSliceLen.Store(int64(DefaultMaxSliceLen))
|
||||
return
|
||||
}
|
||||
maxSliceLen.Store(int64(n))
|
||||
}
|
||||
+152
-95
@@ -119,50 +119,61 @@ func ReadUint16Slice(r Reader, c []uint16) (n int64, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var slice []byte
|
||||
for len(c) > 0 {
|
||||
size := r.Size()
|
||||
if len(c)<<1 < size {
|
||||
size = len(c) << 1
|
||||
}
|
||||
|
||||
size := r.Size()
|
||||
if len(c)<<1 < size {
|
||||
size = len(c) << 1
|
||||
}
|
||||
var slice []byte
|
||||
if slice, err = r.Peek(size); err != nil {
|
||||
return n + int64(len(slice)), err
|
||||
}
|
||||
|
||||
// Then returns the written bytes
|
||||
if slice, err = r.Peek(size); err != nil {
|
||||
return int64(len(slice)), err
|
||||
}
|
||||
buffered := len(slice) >> 1
|
||||
|
||||
buffered := len(slice) >> 1
|
||||
if N := len(c); N <= buffered {
|
||||
|
||||
// If the slice to write on is equal or smaller than the amount peaked
|
||||
if N := len(c); N <= buffered {
|
||||
for i, j := 0, 0; i < N; i, j = i+1, j+2 {
|
||||
c[i] = binary.LittleEndian.Uint16(slice[j:])
|
||||
}
|
||||
|
||||
for i, j := 0, 0; i < N; i, j = i+1, j+2 {
|
||||
nint, derr := r.Discard(N << 1)
|
||||
|
||||
return n + int64(nint), derr
|
||||
}
|
||||
|
||||
if buffered == 0 {
|
||||
return n, ErrZeroProgress
|
||||
}
|
||||
|
||||
for i, j := 0, 0; i < buffered; i, j = i+1, j+2 {
|
||||
c[i] = binary.LittleEndian.Uint16(slice[j:])
|
||||
}
|
||||
|
||||
nint, err := r.Discard(N << 1) // Discards what was read
|
||||
var inc int
|
||||
if inc, err = r.Discard(buffered << 1); err != nil {
|
||||
return n + int64(inc), err
|
||||
}
|
||||
|
||||
return int64(nint), err
|
||||
n += int64(inc)
|
||||
c = c[buffered:]
|
||||
}
|
||||
|
||||
// Decodes the maximum
|
||||
for i, j := 0, 0; i < buffered; i, j = i+1, j+2 {
|
||||
c[i] = binary.LittleEndian.Uint16(slice[j:])
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ReadUint16SliceBounded is the bounded-length variant of ReadUint16Slice.
|
||||
func ReadUint16SliceBounded(r Reader, length int) (out []uint16, n int64, err error) {
|
||||
if length < 0 {
|
||||
return nil, 0, ErrSliceTooLarge
|
||||
}
|
||||
|
||||
// Discard what was peeked
|
||||
var inc int
|
||||
if inc, err = r.Discard(len(slice)); err != nil {
|
||||
return n + int64(inc), err
|
||||
if length > MaxSliceLen() {
|
||||
return nil, 0, ErrSliceTooLarge
|
||||
}
|
||||
|
||||
n += int64(inc)
|
||||
|
||||
// Recurses on the remaining slice to fill
|
||||
var inc64 int64
|
||||
inc64, err = ReadUint16Slice(r, c[buffered:])
|
||||
|
||||
return n + inc64, err
|
||||
out = make([]uint16, length)
|
||||
n, err = ReadUint16Slice(r, out)
|
||||
return out, n, err
|
||||
}
|
||||
|
||||
// ReadUint32 reads a uint32 from r and stores the result into *c.
|
||||
@@ -186,6 +197,9 @@ func ReadUint32(r Reader, c *uint32) (n int64, err error) {
|
||||
}
|
||||
|
||||
// ReadUint32Slice reads a slice of uint32 from r and stores the result into c.
|
||||
//
|
||||
// See ReadUint64Slice for the zero-progress / unbounded-recursion guard.
|
||||
// Same iterative implementation, applied to 4-byte stride.
|
||||
func ReadUint32Slice(r Reader, c []uint32) (n int64, err error) {
|
||||
|
||||
// c is empty, return
|
||||
@@ -193,51 +207,61 @@ func ReadUint32Slice(r Reader, c []uint32) (n int64, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var slice []byte
|
||||
for len(c) > 0 {
|
||||
size := r.Size()
|
||||
if len(c)<<2 < size {
|
||||
size = len(c) << 2
|
||||
}
|
||||
|
||||
// Avoid EOF
|
||||
size := r.Size()
|
||||
if len(c)<<2 < size {
|
||||
size = len(c) << 2
|
||||
}
|
||||
var slice []byte
|
||||
if slice, err = r.Peek(size); err != nil {
|
||||
return n + int64(len(slice)), err
|
||||
}
|
||||
|
||||
// Then returns the written bytes
|
||||
if slice, err = r.Peek(size); err != nil {
|
||||
return int64(len(slice)), err
|
||||
}
|
||||
buffered := len(slice) >> 2
|
||||
|
||||
buffered := len(slice) >> 2
|
||||
if N := len(c); N <= buffered {
|
||||
|
||||
// If the slice to write on is equal or smaller than the amount peaked
|
||||
if N := len(c); N <= buffered {
|
||||
for i, j := 0, 0; i < N; i, j = i+1, j+4 {
|
||||
c[i] = binary.LittleEndian.Uint32(slice[j:])
|
||||
}
|
||||
|
||||
for i, j := 0, 0; i < N; i, j = i+1, j+4 {
|
||||
nint, derr := r.Discard(N << 2)
|
||||
|
||||
return n + int64(nint), derr
|
||||
}
|
||||
|
||||
if buffered == 0 {
|
||||
return n, ErrZeroProgress
|
||||
}
|
||||
|
||||
for i, j := 0, 0; i < buffered; i, j = i+1, j+4 {
|
||||
c[i] = binary.LittleEndian.Uint32(slice[j:])
|
||||
}
|
||||
|
||||
nint, err := r.Discard(N << 2) // Discards what was read
|
||||
var inc int
|
||||
if inc, err = r.Discard(buffered << 2); err != nil {
|
||||
return n + int64(inc), err
|
||||
}
|
||||
|
||||
return int64(nint), err
|
||||
n += int64(inc)
|
||||
c = c[buffered:]
|
||||
}
|
||||
|
||||
// Decodes the maximum
|
||||
for i, j := 0, 0; i < buffered; i, j = i+1, j+4 {
|
||||
c[i] = binary.LittleEndian.Uint32(slice[j:])
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ReadUint32SliceBounded is the bounded-length variant of ReadUint32Slice.
|
||||
func ReadUint32SliceBounded(r Reader, length int) (out []uint32, n int64, err error) {
|
||||
if length < 0 {
|
||||
return nil, 0, ErrSliceTooLarge
|
||||
}
|
||||
|
||||
// Discard what was peeked
|
||||
var inc int
|
||||
if inc, err = r.Discard(len(slice)); err != nil {
|
||||
return n + int64(inc), err
|
||||
if length > MaxSliceLen() {
|
||||
return nil, 0, ErrSliceTooLarge
|
||||
}
|
||||
|
||||
n += int64(inc)
|
||||
|
||||
// Recurses on the remaining slice to fill
|
||||
var inc64 int64
|
||||
inc64, err = ReadUint32Slice(r, c[buffered:])
|
||||
|
||||
return n + inc64, err
|
||||
out = make([]uint32, length)
|
||||
n, err = ReadUint32Slice(r, out)
|
||||
return out, n, err
|
||||
}
|
||||
|
||||
// ReadUint64 reads a uint64 from r and stores the result into c.
|
||||
@@ -261,6 +285,17 @@ func ReadUint64(r Reader, c *uint64) (n int64, err error) {
|
||||
}
|
||||
|
||||
// ReadUint64Slice reads a slice of uint64 from r and stores the result into c.
|
||||
//
|
||||
// Bounded against attacker-controlled wire prefixes: if the underlying
|
||||
// Reader returns a Peek shorter than 8 bytes while c still has elements to
|
||||
// fill, the function returns ErrZeroProgress instead of recursing. Prior to
|
||||
// this guard, a malicious upstream length prefix combined with a sparse
|
||||
// Reader would exhaust the goroutine stack (issue discovered by
|
||||
// luxfi/warp/pulsar FuzzPulseDeserialize, seed ccdb090e0ca0007b).
|
||||
//
|
||||
// The recursion in the original lattigo implementation has been replaced
|
||||
// with an in-place loop, so this function now uses O(1) stack regardless of
|
||||
// input length.
|
||||
func ReadUint64Slice(r Reader, c []uint64) (n int64, err error) {
|
||||
|
||||
// c is empty, return
|
||||
@@ -268,49 +303,71 @@ func ReadUint64Slice(r Reader, c []uint64) (n int64, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var slice []byte
|
||||
for len(c) > 0 {
|
||||
// Avoid EOF: bound the Peek to the smaller of available reader
|
||||
// bytes and the destination remaining bytes.
|
||||
size := r.Size()
|
||||
if len(c)<<3 < size {
|
||||
size = len(c) << 3
|
||||
}
|
||||
|
||||
// Avoid EOF
|
||||
size := r.Size()
|
||||
if len(c)<<3 < size {
|
||||
size = len(c) << 3
|
||||
}
|
||||
var slice []byte
|
||||
if slice, err = r.Peek(size); err != nil {
|
||||
return n + int64(len(slice)), err
|
||||
}
|
||||
|
||||
// Then returns the written bytes
|
||||
if slice, err = r.Peek(size); err != nil {
|
||||
return int64(len(slice)), err
|
||||
}
|
||||
buffered := len(slice) >> 3
|
||||
|
||||
buffered := len(slice) >> 3
|
||||
// If the slice to write on is equal or smaller than the amount peeked
|
||||
if N := len(c); N <= buffered {
|
||||
|
||||
// If the slice to write on is equal or smaller than the amount peaked
|
||||
if N := len(c); N <= buffered {
|
||||
for i, j := 0, 0; i < N; i, j = i+1, j+8 {
|
||||
c[i] = binary.LittleEndian.Uint64(slice[j:])
|
||||
}
|
||||
|
||||
for i, j := 0, 0; i < N; i, j = i+1, j+8 {
|
||||
nint, derr := r.Discard(N << 3) // Discards what was read
|
||||
|
||||
return n + int64(nint), derr
|
||||
}
|
||||
|
||||
// Zero-progress guard: Peek did not return enough bytes to decode
|
||||
// even one uint64 yet c still has elements to fill. Without this,
|
||||
// the original recursive implementation looped forever on
|
||||
// c[0:] == c, and Go's stack growth would eventually panic.
|
||||
if buffered == 0 {
|
||||
return n, ErrZeroProgress
|
||||
}
|
||||
|
||||
// Decodes the maximum
|
||||
for i, j := 0, 0; i < buffered; i, j = i+1, j+8 {
|
||||
c[i] = binary.LittleEndian.Uint64(slice[j:])
|
||||
}
|
||||
|
||||
nint, err := r.Discard(N << 3) // Discards what was read
|
||||
// Discard what was peeked
|
||||
var inc int
|
||||
if inc, err = r.Discard(buffered << 3); err != nil {
|
||||
return n + int64(inc), err
|
||||
}
|
||||
|
||||
return int64(nint), err
|
||||
n += int64(inc)
|
||||
c = c[buffered:]
|
||||
}
|
||||
|
||||
// Decodes the maximum
|
||||
for i, j := 0, 0; i < buffered; i, j = i+1, j+8 {
|
||||
c[i] = binary.LittleEndian.Uint64(slice[j:])
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ReadUint64SliceBounded is a length-prefix-aware wrapper around
|
||||
// ReadUint64Slice that rejects requested lengths exceeding MaxSliceLen()
|
||||
// before allocating the destination. Use this when decoding wire formats
|
||||
// where the length is read from the same untrusted stream.
|
||||
func ReadUint64SliceBounded(r Reader, length int) (out []uint64, n int64, err error) {
|
||||
if length < 0 {
|
||||
return nil, 0, ErrSliceTooLarge
|
||||
}
|
||||
|
||||
// Discard what was peeked
|
||||
var inc int
|
||||
if inc, err = r.Discard(len(slice)); err != nil {
|
||||
return n + int64(inc), err
|
||||
if length > MaxSliceLen() {
|
||||
return nil, 0, ErrSliceTooLarge
|
||||
}
|
||||
|
||||
n += int64(inc)
|
||||
|
||||
// Recurses on the remaining slice to fill
|
||||
var inc64 int64
|
||||
inc64, err = ReadUint64Slice(r, c[buffered:])
|
||||
|
||||
return n + inc64, err
|
||||
out = make([]uint64, length)
|
||||
n, err = ReadUint64Slice(r, out)
|
||||
return out, n, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Lux Industries Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// truncReader is a Reader implementation that exposes a payload shorter
|
||||
// than what its Size() advertises. This mirrors the malicious wire format
|
||||
// discovered by luxfi/warp/pulsar's FuzzPulseDeserialize: a giant outer
|
||||
// length prefix combined with a few bytes of payload. Without the
|
||||
// zero-progress guard, ReadUint{16,32,64}Slice recurses on c[buffered:]
|
||||
// where buffered == 0, exhausting the goroutine stack.
|
||||
type truncReader struct {
|
||||
buf []byte
|
||||
advertise int // value returned by Size(); intentionally != len(buf)
|
||||
off int
|
||||
}
|
||||
|
||||
func newTruncReader(payload []byte, advertise int) *truncReader {
|
||||
return &truncReader{buf: payload, advertise: advertise}
|
||||
}
|
||||
|
||||
func (r *truncReader) Read(p []byte) (int, error) {
|
||||
if r.off >= len(r.buf) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, r.buf[r.off:])
|
||||
r.off += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Size reports the *advertised* size, NOT the actual remaining bytes.
|
||||
// This is what triggered the original CVE: the lattigo path trusts
|
||||
// r.Size() for capping its Peek length.
|
||||
func (r *truncReader) Size() int {
|
||||
remain := r.advertise - r.off
|
||||
if remain < 0 {
|
||||
return 0
|
||||
}
|
||||
return remain
|
||||
}
|
||||
|
||||
// Peek returns up to n bytes of the actual buffer, but does NOT return
|
||||
// io.EOF when the request exceeds the actual buffer — instead it returns
|
||||
// (short, nil). This is the exact misbehavior produced by the warp/pulsar
|
||||
// regression seed; bufio.Reader can also reach this state when the
|
||||
// underlying io.Reader returns (0, nil).
|
||||
func (r *truncReader) Peek(n int) ([]byte, error) {
|
||||
avail := len(r.buf) - r.off
|
||||
if avail >= n {
|
||||
return r.buf[r.off : r.off+n], nil
|
||||
}
|
||||
// Short, non-erroring peek: the bug surface.
|
||||
return r.buf[r.off:], nil
|
||||
}
|
||||
|
||||
func (r *truncReader) Discard(n int) (int, error) {
|
||||
avail := len(r.buf) - r.off
|
||||
if n > avail {
|
||||
r.off = len(r.buf)
|
||||
return avail, io.EOF
|
||||
}
|
||||
r.off += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// TestReadUint64Slice_ZeroProgress is the regression test for the DoS
|
||||
// vulnerability discovered by github.com/luxfi/warp/pulsar's
|
||||
// FuzzPulseDeserialize harness, regression seed ccdb090e0ca0007b. Prior
|
||||
// to the fix in cap.go + the iterative rewrite of ReadUint{16,32,64}Slice,
|
||||
// this test would crash with `runtime: goroutine stack exceeds limit`.
|
||||
func TestReadUint64Slice_ZeroProgress(t *testing.T) {
|
||||
// Two bytes of payload, advertise 1 GiB. After decoding `buffered=0`
|
||||
// elements the buggy code recurses on c[0:] which equals c.
|
||||
r := newTruncReader([]byte{0x41, 0x42}, 1<<30)
|
||||
|
||||
// Destination slice large enough to make the fault observable.
|
||||
c := make([]uint64, 1<<20)
|
||||
|
||||
_, err := ReadUint64Slice(r, c)
|
||||
if !errors.Is(err, ErrZeroProgress) {
|
||||
t.Fatalf("expected ErrZeroProgress, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadUint32Slice_ZeroProgress verifies the same guard for uint32.
|
||||
func TestReadUint32Slice_ZeroProgress(t *testing.T) {
|
||||
r := newTruncReader([]byte{0x01}, 1<<30)
|
||||
c := make([]uint32, 1<<20)
|
||||
|
||||
_, err := ReadUint32Slice(r, c)
|
||||
if !errors.Is(err, ErrZeroProgress) {
|
||||
t.Fatalf("expected ErrZeroProgress, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadUint16Slice_ZeroProgress verifies the same guard for uint16.
|
||||
func TestReadUint16Slice_ZeroProgress(t *testing.T) {
|
||||
r := newTruncReader([]byte{}, 1<<30)
|
||||
c := make([]uint16, 1<<20)
|
||||
|
||||
_, err := ReadUint16Slice(r, c)
|
||||
if !errors.Is(err, ErrZeroProgress) {
|
||||
t.Fatalf("expected ErrZeroProgress, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadUint64Slice_HappyPath ensures the iterative rewrite still
|
||||
// decodes correctly when the Peek returns a partial slice that DOES
|
||||
// permit progress (>= 8 bytes). This guards against regressing the
|
||||
// original lattigo callers, which depend on multi-step decoding of
|
||||
// large polynomials over a chunked Reader.
|
||||
func TestReadUint64Slice_HappyPath(t *testing.T) {
|
||||
const N = 16
|
||||
payload := make([]byte, N*8)
|
||||
for i := 0; i < N; i++ {
|
||||
// Little-endian uint64 with i+1 in the low byte.
|
||||
payload[i*8] = byte(i + 1)
|
||||
}
|
||||
|
||||
// Buffer-backed reader exercises the in-tree happy path.
|
||||
b := NewBuffer(payload)
|
||||
c := make([]uint64, N)
|
||||
|
||||
n, err := ReadUint64Slice(b, c)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadUint64Slice: %v", err)
|
||||
}
|
||||
if n != int64(N*8) {
|
||||
t.Fatalf("read %d bytes, want %d", n, N*8)
|
||||
}
|
||||
for i := 0; i < N; i++ {
|
||||
if c[i] != uint64(i+1) {
|
||||
t.Fatalf("c[%d] = %d, want %d", i, c[i], i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadUint64SliceBounded_RejectsHugeLength asserts that the
|
||||
// length-bounded wrapper rejects attacker-controlled slice lengths
|
||||
// before any Peek is issued.
|
||||
func TestReadUint64SliceBounded_RejectsHugeLength(t *testing.T) {
|
||||
r := NewBuffer([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08})
|
||||
_, _, err := ReadUint64SliceBounded(r, MaxSliceLen()+1)
|
||||
if !errors.Is(err, ErrSliceTooLarge) {
|
||||
t.Fatalf("expected ErrSliceTooLarge, got %v", err)
|
||||
}
|
||||
_, _, err = ReadUint64SliceBounded(r, -1)
|
||||
if !errors.Is(err, ErrSliceTooLarge) {
|
||||
t.Fatalf("expected ErrSliceTooLarge for negative, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetMaxSliceLen confirms the cap can be tightened at runtime and
|
||||
// reset to default with n <= 0.
|
||||
func TestSetMaxSliceLen(t *testing.T) {
|
||||
t.Cleanup(func() { SetMaxSliceLen(0) })
|
||||
|
||||
SetMaxSliceLen(64)
|
||||
if got := MaxSliceLen(); got != 64 {
|
||||
t.Fatalf("MaxSliceLen() = %d, want 64", got)
|
||||
}
|
||||
|
||||
SetMaxSliceLen(0) // reset
|
||||
if got := MaxSliceLen(); got != DefaultMaxSliceLen {
|
||||
t.Fatalf("MaxSliceLen() = %d, want %d after reset", got, DefaultMaxSliceLen)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user