Files
zeekayandHanzo Dev 44b48c7717 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]>
2026-08-26 10:41:31 -07:00

78 lines
2.5 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"bytes"
"encoding/binary"
"errors"
"testing"
"github.com/luxfi/math/codec"
)
// 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))
}
return buf.Bytes()
}
// 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) {
// The value from the original fuzz finding.
const huge = uint64(70_368_955_777_453)
var buf bytes.Buffer
_ = binary.Write(&buf, binary.LittleEndian, huge)
err := ValidateVectorPolyFrame(buf.Bytes())
if err == nil {
t.Fatal("a frame declaring 70 trillion polys was accepted")
}
if !errors.Is(err, codec.ErrLimitExceeded) {
t.Errorf("err is not ErrLimitExceeded: %v", err)
}
}
// TestValidateVectorPolyFrame_HappyPath accepts a frame whose counts are all
// backed by bytes that are there.
func TestValidateVectorPolyFrame_HappyPath(t *testing.T) {
if err := ValidateVectorPolyFrame(vectorFrame(3, 1, 16)); err != nil {
t.Errorf("happy-path: %v", err)
}
}
// TestValidateVectorPolyFrame_AtCap accepts exactly MaxLatticeUintSliceLen
// coefficients, which is the largest shape corona will read.
func TestValidateVectorPolyFrame_AtCap(t *testing.T) {
if err := ValidateVectorPolyFrame(vectorFrame(1, 1, MaxLatticeUintSliceLen)); err != nil {
t.Errorf("at-cap: %v", err)
}
}
// 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) {
err := ValidateVectorPolyFrame(vectorFrame(1, 1, MaxLatticeUintSliceLen+1))
if err == nil {
t.Fatal("over-cap returned nil")
}
if !errors.Is(err, codec.ErrLimitExceeded) {
t.Errorf("err is not ErrLimitExceeded: %v", err)
}
}