mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
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]>
78 lines
2.5 KiB
Go
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)
|
|
}
|
|
}
|