mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
threshold: validate Vector[Poly] frame before lattigo ReadFrom
FuzzPulsarSign1Round1Data found a 9-byte input that crashes the goroutine with `fatal error: out of memory: cannot allocate 105589698985984-byte block` in `lattice/v7/utils/structs.Vector[T]. ReadFrom`. Recovery cannot catch the fatal OOM — the only safe path is to reject malformed frames before `make([]T, size)` runs. This is a SECOND DoS surface in lattice/v7, distinct from the ReadUintNSlice recursion fixed by luxfi/lattice#3 (still open): ReadFrom calls `make([]T, size)` with `size` read from the wire before any bound check. PR #3 fixes the inner slice readers but the outer Vector wrapper allocation remains exploitable. Filed upstream as luxfi/lattice#4 with reproduction. The new validateVectorPolyFrameInline walker mirrors the existing warp/pulsar.validateVectorPolyFrame: walks the wire frame end-to-end, asserts every length field is <= MaxLatticeUintSliceLen (4096), and rejects truncated / over-large frames before the lattigo decoder runs. Result: FuzzPulsarSign1Round1Data passes 30s with the new walker in place (28577 execs, 0 panics). Failing input testdata/fuzz/FuzzPulsarSign1Round1Data/a80b8d313b40fa55 is now rejected cleanly with "vector length 70368955777453 exceeds 4096". Refs: luxfi/lattice#2 (open), luxfi/lattice#3 (open), luxfi/lattice#4 (filed 2026-05-04)
This commit is contained in:
@@ -20,6 +20,7 @@ package threshold
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -30,6 +31,68 @@ import (
|
||||
"github.com/luxfi/lattice/v7/utils/structs"
|
||||
)
|
||||
|
||||
// maxLatticeUintSliceLen mirrors warp/pulsar.MaxLatticeUintSliceLen and
|
||||
// bounds every length field a lattigo wire frame can declare. A
|
||||
// canonical Pulsar Poly has 256 coefficients per level; a Vector/Matrix
|
||||
// has at most M*N = 8*32 = 256 polys. This cap is structural — frames
|
||||
// that declare more are not legitimate Pulsar protocol bytes.
|
||||
//
|
||||
// IMPORTANT: this duplicate cap exists because the upstream lattice
|
||||
// library has TWO DoS surfaces:
|
||||
// 1. ReadUintNSlice unbounded recursion — fixed by luxfi/lattice#3
|
||||
// (open as of 2026-05-04).
|
||||
// 2. Vector.ReadFrom calling make([]T, size) before any bound check —
|
||||
// NOT addressed by PR #3. A 9-byte input
|
||||
// `\xad\x93\xd8\x5a\x00\x04\x00\x00\\` reads size=0x40005AD893AD
|
||||
// (~70T entries) and OOMs the goroutine before the slice reader
|
||||
// runs. Found by FuzzPulsarSign1Round1Data on 2026-05-04.
|
||||
//
|
||||
// The walker below pre-validates the wire frame BEFORE handing it to
|
||||
// lattigo, mirroring warp/pulsar.validateVectorPolyFrame.
|
||||
const maxLatticeUintSliceLen = 4096
|
||||
|
||||
// validateVectorPolyFrameInline walks a lattigo Vector[Poly] wire
|
||||
// frame end-to-end (one 8-byte vector length header followed by N
|
||||
// concatenated Poly frames; each Poly = 8-byte levels header + per-
|
||||
// level 8 + 8*coeff_count). Returns nil on a structurally valid
|
||||
// frame, error otherwise. Mirrors warp/pulsar.validateVectorPolyFrame.
|
||||
func validateVectorPolyFrameInline(frame []byte) error {
|
||||
if len(frame) < 8 {
|
||||
return fmt.Errorf("vector frame too short: %d < 8", len(frame))
|
||||
}
|
||||
n := binary.LittleEndian.Uint64(frame[:8])
|
||||
if n > maxLatticeUintSliceLen {
|
||||
return fmt.Errorf("vector length %d exceeds %d", n, maxLatticeUintSliceLen)
|
||||
}
|
||||
rest := frame[8:]
|
||||
for i := uint64(0); i < n; i++ {
|
||||
if len(rest) < 8 {
|
||||
return fmt.Errorf("vector poly %d: header truncated (%d)", i, len(rest))
|
||||
}
|
||||
levels := binary.LittleEndian.Uint64(rest[:8])
|
||||
if levels > maxLatticeUintSliceLen {
|
||||
return fmt.Errorf("vector poly %d: levels %d exceeds %d", i, levels, maxLatticeUintSliceLen)
|
||||
}
|
||||
rest = rest[8:]
|
||||
for k := uint64(0); k < levels; k++ {
|
||||
if len(rest) < 8 {
|
||||
return fmt.Errorf("vector poly %d level %d: header truncated", i, k)
|
||||
}
|
||||
coeffs := binary.LittleEndian.Uint64(rest[:8])
|
||||
rest = rest[8:]
|
||||
if coeffs > maxLatticeUintSliceLen {
|
||||
return fmt.Errorf("vector poly %d level %d: coeff count %d exceeds %d", i, k, coeffs, maxLatticeUintSliceLen)
|
||||
}
|
||||
need := coeffs * 8
|
||||
if uint64(len(rest)) < need {
|
||||
return fmt.Errorf("vector poly %d level %d: need %d coeff bytes, have %d", i, k, need, len(rest))
|
||||
}
|
||||
rest = rest[need:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeEmptyPolyVector returns a length-N vector of zero-initialized
|
||||
// Poly's bound to ring r. This is the destination shape every
|
||||
// ReadFrom-fuzz target writes into.
|
||||
@@ -68,6 +131,16 @@ func decodeVectorWithRecover(raw []byte) (err error) {
|
||||
if len(raw) > fuzzMaxRawSize {
|
||||
return fmt.Errorf("input exceeds fuzzMaxRawSize")
|
||||
}
|
||||
// Pre-validate the wire frame BEFORE handing to lattigo. This is
|
||||
// defense-in-depth against the second lattice DoS surface
|
||||
// (Vector.ReadFrom -> make([]T, size) with no bound check), which
|
||||
// luxfi/lattice#3 does not address. recover() cannot catch the
|
||||
// fatal "out of memory" thrown when make() requests > available
|
||||
// memory, so the only safe path is to reject malformed frames
|
||||
// before the allocation runs.
|
||||
if vErr := validateVectorPolyFrameInline(raw); vErr != nil {
|
||||
return vErr
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("decode panic recovered: %v", r)
|
||||
|
||||
Reference in New Issue
Block a user