mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
corona: a length a peer wrote has to be backed by bytes that are there
Decoding a signature or a group key means reading counts a peer chose, and the lattice decoder allocates from a count before it discovers there is nothing to fill the space with. A fifty-byte message naming 2^40 coefficients ended the process — sometimes with "makeslice: len out of range", and for a challenge naming 2^40 levels by allocating for seventy seconds until the kernel killed it. ValidateVectorPolyFrame was meant to stop that and did not. Its doc said it checked "vector outer length, per-poly levels count, per-level coefficient count"; its body read the outer length, discarded the result, and returned nil, which its own comment admitted was a stopgap. So it accepted every frame above. It now walks the frame. The layout is a little-endian count, then per poly a level count, a coefficient count, and the coefficients; the rule at each count is the one readLenPrefixed already applies to the outer frame — what is declared has to fit in what remains. That cannot refuse an honest frame, because an honest frame's bytes are all present. A level count times a coefficient count is compared by dividing rather than multiplying, so a product that would wrap is refused instead of wrapping into a small number that passes. Two frames were being decoded without ever being walked: a signature's challenge C, on the grounds that a single Poly is "bounded by the ring size already" — it is not, it names its own counts — and a group key's A, which is a matrix and carries the same counts a row deeper. Both are walked now. The tracked tests built frames with uvarint length prefixes, which is not what lattigo writes, so what they accepted was a shape no peer could send and what they proved was nothing about a real frame. They now build the real layout, and every walk is controlled by a frame the library itself marshalled — without that control the refusals would only prove this rejects everything. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package threshold
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAPeerCannotEndTheProcessWithALength. Decoding a signature means reading
|
||||
// counts a peer wrote, and the lattice decoder allocates from a count before it
|
||||
// discovers there is nothing to fill the space with. A fifty-byte message
|
||||
// naming 2^40 coefficients ended the process with "makeslice: len out of
|
||||
// range" — a node killed by reading its peer's signature.
|
||||
func TestAPeerCannotEndTheProcessWithALength(t *testing.T) {
|
||||
le := func(n uint64) []byte {
|
||||
var b [8]byte
|
||||
binary.LittleEndian.PutUint64(b[:], n)
|
||||
return b[:]
|
||||
}
|
||||
lenPrefixed := func(body []byte) []byte {
|
||||
var l [4]byte
|
||||
binary.BigEndian.PutUint32(l[:], uint32(len(body)))
|
||||
return append(l[:], body...)
|
||||
}
|
||||
// A vector naming polys, levels and coefficients, with no bytes behind them.
|
||||
claim := func(polys, levels, n uint64) []byte {
|
||||
return append(append(le(polys), le(levels)...), le(n)...)
|
||||
}
|
||||
// C is a single Poly, not a vector, so it names only levels and
|
||||
// coefficients.
|
||||
polyClaim := func(levels, n uint64) []byte {
|
||||
return append(le(levels), le(n)...)
|
||||
}
|
||||
sigFrame := func(c, z, delta []byte) []byte {
|
||||
var out []byte
|
||||
var m [4]byte
|
||||
binary.BigEndian.PutUint32(m[:], wireMagicSig)
|
||||
out = append(out, m[:]...)
|
||||
var v [2]byte
|
||||
binary.BigEndian.PutUint16(v[:], wireVersionV1)
|
||||
out = append(out, v[:]...)
|
||||
out = append(out, lenPrefixed(c)...)
|
||||
out = append(out, lenPrefixed(z)...)
|
||||
return append(out, lenPrefixed(delta)...)
|
||||
}
|
||||
|
||||
honest := claim(1, 1, 0)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
frame []byte
|
||||
}{
|
||||
{"Z names 2^40 coefficients", sigFrame(honest, claim(1, 1, 1<<40), honest)},
|
||||
{"Delta names 2^40 coefficients", sigFrame(honest, honest, claim(1, 1, 1<<40))},
|
||||
{"Z names 2^40 polys", sigFrame(honest, claim(1<<40, 1, 1), honest)},
|
||||
{"Z names 2^40 levels", sigFrame(honest, claim(1, 1<<40, 1), honest)},
|
||||
{"C names 2^40 coefficients", sigFrame(polyClaim(1, 1<<40), honest, honest)},
|
||||
{"C names 2^40 levels", sigFrame(polyClaim(1<<40, 1), honest, honest)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("%d bytes from a peer ended the process: %v", len(tc.frame), r)
|
||||
}
|
||||
}()
|
||||
var s Signature
|
||||
if err := s.UnmarshalBinary(tc.frame); err == nil {
|
||||
t.Fatal("a frame describing bytes that are not there was accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A group key's A is a matrix and was decoded without ever being walked, so it
|
||||
// could name rows and coefficients that were not there.
|
||||
func TestAGroupKeysMatrixCannotEndTheProcess(t *testing.T) {
|
||||
le := func(n uint64) []byte {
|
||||
var b [8]byte
|
||||
binary.LittleEndian.PutUint64(b[:], n)
|
||||
return b[:]
|
||||
}
|
||||
lenPrefixed := func(body []byte) []byte {
|
||||
var l [4]byte
|
||||
binary.BigEndian.PutUint32(l[:], uint32(len(body)))
|
||||
return append(l[:], body...)
|
||||
}
|
||||
keyFrame := func(a, b []byte) []byte {
|
||||
var out []byte
|
||||
var m [4]byte
|
||||
binary.BigEndian.PutUint32(m[:], wireMagicGroupKey)
|
||||
out = append(out, m[:]...)
|
||||
var v [2]byte
|
||||
binary.BigEndian.PutUint16(v[:], wireVersionV1)
|
||||
out = append(out, v[:]...)
|
||||
out = append(out, lenPrefixed(a)...)
|
||||
return append(out, lenPrefixed(b)...)
|
||||
}
|
||||
honestVec := append(append(le(1), le(1)...), le(0)...)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
a []byte
|
||||
}{
|
||||
{"2^40 rows", le(1 << 40)},
|
||||
{"one row of 2^40 polys", append(le(1), le(1<<40)...)},
|
||||
{"one poly of 2^40 coefficients", append(append(le(1), le(1)...), append(le(1), le(1<<40)...)...)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("a group key from a peer ended the process: %v", r)
|
||||
}
|
||||
}()
|
||||
var gk GroupKey
|
||||
if err := gk.UnmarshalBinary(keyFrame(tc.a, honestVec)); err == nil {
|
||||
t.Fatal("a matrix describing bytes that are not there was accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -122,8 +122,14 @@ func (s *Signature) UnmarshalBinary(b []byte) error {
|
||||
return fmt.Errorf("corona/wire: %d trailing bytes after Signature", r.Len())
|
||||
}
|
||||
|
||||
// Bounded validation of Z, Delta vector frames (C is a single Poly
|
||||
// which is bounded by the ring size already).
|
||||
// Every frame is walked before it is decoded, because the decoder allocates
|
||||
// from the counts these carry. C is a single Poly and names its own level
|
||||
// and coefficient counts, so it describes bytes that are not there exactly
|
||||
// as a vector can — a twenty-four byte C naming 2^40 levels does not panic,
|
||||
// it allocates until the process is killed.
|
||||
if err := wire.ValidatePolyFrame(cBytes); err != nil {
|
||||
return fmt.Errorf("%w: C: %v", ErrWireFrameRejected, err)
|
||||
}
|
||||
if err := wire.ValidateVectorPolyFrame(zBytes); err != nil {
|
||||
return fmt.Errorf("%w: Z: %v", ErrWireFrameRejected, err)
|
||||
}
|
||||
@@ -222,6 +228,11 @@ func (gk *GroupKey) UnmarshalBinary(b []byte) error {
|
||||
return fmt.Errorf("corona/wire: %d trailing bytes after GroupKey", r.Len())
|
||||
}
|
||||
|
||||
// A is a matrix and carries the same kind of counts as everything else here,
|
||||
// so it is walked before it is decoded rather than after.
|
||||
if err := wire.ValidateMatrixPolyFrame(aBytes); err != nil {
|
||||
return fmt.Errorf("%w: A: %v", ErrWireFrameRejected, err)
|
||||
}
|
||||
if err := wire.ValidateVectorPolyFrame(bBytes); err != nil {
|
||||
return fmt.Errorf("%w: BTilde: %v", ErrWireFrameRejected, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package wire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
"github.com/luxfi/lattice/v7/utils/structs"
|
||||
"github.com/luxfi/math/codec"
|
||||
)
|
||||
|
||||
func realVector(t *testing.T, polys int) []byte {
|
||||
t.Helper()
|
||||
r, err := ring.NewRing(256, []uint64{8380417})
|
||||
if err != nil {
|
||||
t.Fatalf("ring: %v", err)
|
||||
}
|
||||
v := make(structs.Vector[ring.Poly], polys)
|
||||
for i := range v {
|
||||
v[i] = r.NewPoly()
|
||||
}
|
||||
b, err := v.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestFramesTheLibraryWroteAreAccepted is the control. Without it the refusals
|
||||
// below would prove only that this rejects everything.
|
||||
func TestFramesTheLibraryWroteAreAccepted(t *testing.T) {
|
||||
for _, polys := range []int{0, 1, 2, 5} {
|
||||
if err := ValidateVectorPolyFrame(realVector(t, polys)); err != nil {
|
||||
t.Fatalf("a frame the library wrote, %d polys, was refused: %v", polys, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestADeclaredLengthMustBeBackedByBytes. A peer writes the counts in this
|
||||
// frame, and the decoder beneath allocates from them before discovering there
|
||||
// is nothing to fill the space with: a fifty-byte message naming 2^40
|
||||
// coefficients used to end the process with "makeslice: len out of range".
|
||||
func TestADeclaredLengthMustBeBackedByBytes(t *testing.T) {
|
||||
u64 := func(n uint64) []byte {
|
||||
var b [8]byte
|
||||
binary.LittleEndian.PutUint64(b[:], n)
|
||||
return b[:]
|
||||
}
|
||||
frame := func(parts ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, p := range parts {
|
||||
out = append(out, p...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
frame []byte
|
||||
}{
|
||||
{"a vector of 2^40 polys", frame(u64(1 << 40))},
|
||||
{"a poly of 2^40 coefficients", frame(u64(1), u64(1), u64(1<<40))},
|
||||
{"2^40 levels", frame(u64(1), u64(1<<40), u64(1))},
|
||||
{"levels times coefficients would wrap", frame(u64(1), u64(1<<62), u64(4))},
|
||||
{"one more poly than there are bytes for", frame(u64(2), u64(1), u64(1), u64(0))},
|
||||
{"a count with no room for its own bytes", frame([]byte{1, 2, 3})},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ValidateVectorPolyFrame(tc.frame)
|
||||
if err == nil {
|
||||
t.Fatalf("%d bytes declaring far more were accepted", len(tc.frame))
|
||||
}
|
||||
if !errors.Is(err, codec.ErrLimitExceeded) {
|
||||
t.Fatalf("refused for the wrong reason: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes past the end of what the frame describes are bytes two different frames
|
||||
// can differ in while decoding the same.
|
||||
func TestTrailingBytesAreRefused(t *testing.T) {
|
||||
padded := append(realVector(t, 1), 0)
|
||||
if err := ValidateVectorPolyFrame(padded); err == nil {
|
||||
t.Fatal("a frame with a byte appended was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatricesTheLibraryWroteAreAccepted is the control for the matrix walk: if
|
||||
// the layout assumed here were wrong, this would refuse what lattigo writes.
|
||||
func TestMatricesTheLibraryWroteAreAccepted(t *testing.T) {
|
||||
r, err := ring.NewRing(256, []uint64{8380417})
|
||||
if err != nil {
|
||||
t.Fatalf("ring: %v", err)
|
||||
}
|
||||
for _, dim := range [][2]int{{1, 1}, {2, 3}, {0, 0}} {
|
||||
m := make(structs.Matrix[ring.Poly], dim[0])
|
||||
for i := range m {
|
||||
m[i] = make(structs.Vector[ring.Poly], dim[1])
|
||||
for j := range m[i] {
|
||||
m[i][j] = r.NewPoly()
|
||||
}
|
||||
}
|
||||
b, err := m.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal %v: %v", dim, err)
|
||||
}
|
||||
if err := ValidateMatrixPolyFrame(b); err != nil {
|
||||
t.Fatalf("a %dx%d matrix the library wrote was refused: %v", dim[0], dim[1], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
-28
@@ -17,7 +17,7 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/math/codec"
|
||||
@@ -42,38 +42,139 @@ var LatticeWireLimits = codec.Limits{
|
||||
MaxDepth: 4,
|
||||
}
|
||||
|
||||
// ValidateVectorPolyFrame walks a lattigo Vector[Poly] wire frame
|
||||
// without invoking lattigo's recursive ReadUint64Slice. Returns nil
|
||||
// iff every length-prefixed slice (vector outer length, per-poly
|
||||
// levels count, per-level coefficient count) is within
|
||||
// MaxLatticeUintSliceLen.
|
||||
// ValidateVectorPolyFrame walks a lattigo Vector[Poly] frame and returns nil
|
||||
// only when every length it declares is backed by bytes that are actually
|
||||
// there.
|
||||
//
|
||||
// Mirrors warp/pulsar.validateVectorPolyFrame and consolidates the
|
||||
// test-only walker that lived at threshold/fuzz_round_test.go onto
|
||||
// the canonical luxfi/math/codec substrate.
|
||||
// The frame is little-endian and self-describing:
|
||||
//
|
||||
// On rejection, the returned error wraps codec.ErrLimitExceeded so
|
||||
// callers can branch on errors.Is.
|
||||
// vector := count:u64, count × poly
|
||||
// poly := levels:u64, n:u64, levels*n × coefficient:u64
|
||||
// matrix := rows:u64, rows × vector
|
||||
//
|
||||
// A peer writes those counts, so a short frame can declare an enormous one —
|
||||
// and the decoder beneath this allocates from the declared count before it
|
||||
// discovers there is nothing to fill it with. A fifty-byte message naming 2^40
|
||||
// coefficients ends the process: sometimes with "makeslice: len out of range",
|
||||
// and sometimes by allocating until the kernel kills it. Reading a peer's
|
||||
// signature must not be able to do either.
|
||||
//
|
||||
// So the rule is the one readLenPrefixed already applies to the outer frame:
|
||||
// what is declared has to fit in what remains. That cannot refuse an honest
|
||||
// frame, since an honest frame's bytes are all present, and it refuses every
|
||||
// frame whose arithmetic does not close.
|
||||
//
|
||||
// MaxLatticeUintSliceLen still caps the shape corona itself will accept, above
|
||||
// and beyond what the bytes could hold.
|
||||
func ValidateVectorPolyFrame(frame []byte) error {
|
||||
r, err := codec.NewReader(bytes.NewReader(frame), LatticeWireLimits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pulsar/wire: NewReader: %w", err)
|
||||
if err := frameFits(frame); err != nil {
|
||||
return err
|
||||
}
|
||||
// Outer vector length.
|
||||
vec, err := r.ReadUint64Slice()
|
||||
rest, err := walkVector(frame)
|
||||
if err != nil {
|
||||
// codec.Reader's bounded ReadUint64Slice rejects the lattice
|
||||
// issue #4 attack input class (huge length) before allocation;
|
||||
// surface that rejection as a substrate-validated error.
|
||||
return fmt.Errorf("pulsar/wire: outer vector length: %w", err)
|
||||
return err
|
||||
}
|
||||
return noTrailing(rest, "vector")
|
||||
}
|
||||
|
||||
// ValidatePolyFrame is the same walk for a single Poly, which is what a
|
||||
// Signature's challenge is. A Poly names its own level and coefficient counts,
|
||||
// so it describes bytes that are not there exactly as a vector can.
|
||||
func ValidatePolyFrame(frame []byte) error {
|
||||
if err := frameFits(frame); err != nil {
|
||||
return err
|
||||
}
|
||||
rest, err := walkPoly(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return noTrailing(rest, "poly")
|
||||
}
|
||||
|
||||
// ValidateMatrixPolyFrame is the same walk for a Matrix[Poly], a count followed
|
||||
// by that many vectors. A group key's A is one.
|
||||
func ValidateMatrixPolyFrame(frame []byte) error {
|
||||
if err := frameFits(frame); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, rest, err := takeCount(frame)
|
||||
if err != nil {
|
||||
return fmt.Errorf("matrix row count: %w", err)
|
||||
}
|
||||
for i := uint64(0); i < rows; i++ {
|
||||
if rest, err = walkVector(rest); err != nil {
|
||||
return fmt.Errorf("row %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return noTrailing(rest, "matrix")
|
||||
}
|
||||
|
||||
func frameFits(frame []byte) error {
|
||||
if len(frame) > LatticeWireLimits.MaxFrameBytes {
|
||||
return fmt.Errorf("%w: frame is %d bytes, limit %d",
|
||||
codec.ErrLimitExceeded, len(frame), LatticeWireLimits.MaxFrameBytes)
|
||||
}
|
||||
// vec is the FIRST slice in the frame, but lattigo's
|
||||
// Vector[Poly] format actually nests Poly structs after the
|
||||
// length prefix, not raw uint64 elements. We re-interpret the
|
||||
// outer-length read as "number of Poly entries to follow" by
|
||||
// swallowing only the varint length and discarding the payload
|
||||
// read. Future Phase 5 work tightens this when we move pulsar
|
||||
// fully onto math/codec; for now this is a hardened bound check.
|
||||
_ = vec
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bytes past the end of what a frame describes are bytes two frames can differ
|
||||
// in while decoding the same.
|
||||
func noTrailing(rest []byte, what string) error {
|
||||
if len(rest) != 0 {
|
||||
return fmt.Errorf("%w: %d trailing bytes after the %s", codec.ErrLimitExceeded, len(rest), what)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// takeCount reads one little-endian count and reports what follows it. A count
|
||||
// is refused when the bytes left could not hold that many of anything, since
|
||||
// every element costs at least one byte.
|
||||
func takeCount(b []byte) (uint64, []byte, error) {
|
||||
if len(b) < 8 {
|
||||
return 0, nil, fmt.Errorf("%w: %d bytes left, a count needs 8", codec.ErrLimitExceeded, len(b))
|
||||
}
|
||||
n := binary.LittleEndian.Uint64(b[:8])
|
||||
rest := b[8:]
|
||||
if n > uint64(len(rest)) {
|
||||
return 0, nil, fmt.Errorf("%w: declared %d, only %d bytes remain", codec.ErrLimitExceeded, n, len(rest))
|
||||
}
|
||||
if n > MaxLatticeUintSliceLen {
|
||||
return 0, nil, fmt.Errorf("%w: declared %d, corona accepts at most %d",
|
||||
codec.ErrLimitExceeded, n, MaxLatticeUintSliceLen)
|
||||
}
|
||||
return n, rest, nil
|
||||
}
|
||||
|
||||
func walkVector(b []byte) ([]byte, error) {
|
||||
count, rest, err := takeCount(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vector length: %w", err)
|
||||
}
|
||||
for i := uint64(0); i < count; i++ {
|
||||
if rest, err = walkPoly(rest); err != nil {
|
||||
return nil, fmt.Errorf("poly %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return rest, nil
|
||||
}
|
||||
|
||||
func walkPoly(b []byte) ([]byte, error) {
|
||||
levels, rest, err := takeCount(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level count: %w", err)
|
||||
}
|
||||
n, rest, err := takeCount(rest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("coefficient count: %w", err)
|
||||
}
|
||||
// levels*n*8 without trusting it to fit: divide instead of multiply, so a
|
||||
// product that would wrap is refused rather than wrapping into a small
|
||||
// number that passes.
|
||||
const coeffBytes = 8
|
||||
room := uint64(len(rest)) / coeffBytes
|
||||
if n != 0 && levels > room/n {
|
||||
return nil, fmt.Errorf("%w: %d levels of %d coefficients need more than the %d bytes left",
|
||||
codec.ErrLimitExceeded, levels, n, len(rest))
|
||||
}
|
||||
return rest[levels*n*coeffBytes:], nil
|
||||
}
|
||||
|
||||
+29
-37
@@ -12,70 +12,62 @@ import (
|
||||
"github.com/luxfi/math/codec"
|
||||
)
|
||||
|
||||
func encodeUvarint(out *bytes.Buffer, v uint64) {
|
||||
for v >= 0x80 {
|
||||
out.WriteByte(byte(v) | 0x80)
|
||||
v >>= 7
|
||||
// vectorFrame writes the frame lattigo writes: a little-endian count followed
|
||||
// by that many polys, each a level count, a coefficient count, and the
|
||||
// coefficients themselves. Tests build frames this way so that what they accept
|
||||
// is what a peer can actually send.
|
||||
func vectorFrame(polys, levels, n uint64) []byte {
|
||||
var buf bytes.Buffer
|
||||
le := func(v uint64) { _ = binary.Write(&buf, binary.LittleEndian, v) }
|
||||
le(polys)
|
||||
for i := uint64(0); i < polys; i++ {
|
||||
le(levels)
|
||||
le(n)
|
||||
buf.Write(make([]byte, levels*n*8))
|
||||
}
|
||||
out.WriteByte(byte(v))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// TestValidateVectorPolyFrame_RejectsHugeLength is the regression
|
||||
// test for lattice issue #4 (Vector[T].ReadFrom unbounded allocation),
|
||||
// now centralized through luxfi/math/codec.Reader. The exact attack
|
||||
// input from the original fuzz finding (9-byte varint encoding of a
|
||||
// 70-trillion-element length) MUST be rejected with
|
||||
// codec.ErrLimitExceeded.
|
||||
// TestValidateVectorPolyFrame_RejectsHugeLength is the regression test for
|
||||
// lattice issue #4 (Vector[T].ReadFrom unbounded allocation): a short frame
|
||||
// naming an enormous count must be refused rather than allocated from.
|
||||
func TestValidateVectorPolyFrame_RejectsHugeLength(t *testing.T) {
|
||||
// Length: 70_368_955_777_453 — same value used in lattice issue #4.
|
||||
// The value from the original fuzz finding.
|
||||
const huge = uint64(70_368_955_777_453)
|
||||
|
||||
var buf bytes.Buffer
|
||||
encodeUvarint(&buf, huge)
|
||||
_ = binary.Write(&buf, binary.LittleEndian, huge)
|
||||
|
||||
err := ValidateVectorPolyFrame(buf.Bytes())
|
||||
if err == nil {
|
||||
t.Fatal("ValidateVectorPolyFrame returned nil for huge length")
|
||||
t.Fatal("a frame declaring 70 trillion polys was accepted")
|
||||
}
|
||||
if !errors.Is(err, codec.ErrLimitExceeded) {
|
||||
t.Errorf("err is not ErrLimitExceeded: %v", err)
|
||||
}
|
||||
t.Logf("rejected as expected: %v", err)
|
||||
}
|
||||
|
||||
// TestValidateVectorPolyFrame_HappyPath confirms the bounded reader
|
||||
// accepts a length within cap.
|
||||
// TestValidateVectorPolyFrame_HappyPath accepts a frame whose counts are all
|
||||
// backed by bytes that are there.
|
||||
func TestValidateVectorPolyFrame_HappyPath(t *testing.T) {
|
||||
want := []uint64{1, 2, 3}
|
||||
var buf bytes.Buffer
|
||||
encodeUvarint(&buf, uint64(len(want)))
|
||||
for _, v := range want {
|
||||
_ = binary.Write(&buf, binary.LittleEndian, v)
|
||||
}
|
||||
if err := ValidateVectorPolyFrame(buf.Bytes()); err != nil {
|
||||
if err := ValidateVectorPolyFrame(vectorFrame(3, 1, 16)); err != nil {
|
||||
t.Errorf("happy-path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateVectorPolyFrame_AtCap accepts exactly MaxLatticeUintSliceLen.
|
||||
// TestValidateVectorPolyFrame_AtCap accepts exactly MaxLatticeUintSliceLen
|
||||
// coefficients, which is the largest shape corona will read.
|
||||
func TestValidateVectorPolyFrame_AtCap(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
encodeUvarint(&buf, uint64(MaxLatticeUintSliceLen))
|
||||
// Provide a payload large enough to satisfy the read; we don't
|
||||
// care about its content, only that it doesn't exceed
|
||||
// MaxFrameBytes.
|
||||
payload := make([]byte, MaxLatticeUintSliceLen*8)
|
||||
buf.Write(payload)
|
||||
if err := ValidateVectorPolyFrame(buf.Bytes()); err != nil {
|
||||
if err := ValidateVectorPolyFrame(vectorFrame(1, 1, MaxLatticeUintSliceLen)); err != nil {
|
||||
t.Errorf("at-cap: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateVectorPolyFrame_OverCap rejects MaxLatticeUintSliceLen + 1.
|
||||
// TestValidateVectorPolyFrame_OverCap rejects MaxLatticeUintSliceLen + 1 even
|
||||
// when the bytes to back it are present, because that is a shape corona does
|
||||
// not read whatever the sender is willing to send.
|
||||
func TestValidateVectorPolyFrame_OverCap(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
encodeUvarint(&buf, uint64(MaxLatticeUintSliceLen+1))
|
||||
err := ValidateVectorPolyFrame(buf.Bytes())
|
||||
err := ValidateVectorPolyFrame(vectorFrame(1, 1, MaxLatticeUintSliceLen+1))
|
||||
if err == nil {
|
||||
t.Fatal("over-cap returned nil")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user