check / check (push) Waiting to run
FuzzNewManifestFromReader fails when the parser returns both or neither of a manifest and an error, or allocates more than a fixed multiple of its input and the decompressed data it may read, plus room for the decoder's window buffers. make test runs the seed corpus; make fuzz fuzzes for one minute. Parser bug: MaxDecompressedSize did not bound decompression; the zstd decoder decoded a payload under 128 KiB in full before the LimitReader read any of it. It now decodes only what the LimitReader reads and refuses windows over the 8 MiB mfer writes with. Seeds: a frame claiming 8 GiB, two frames together over the limit, empty frames with growing windows. Model: opus-5-5
84 lines
3.0 KiB
Go
84 lines
3.0 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package mfer
|
|
|
|
import (
|
|
"bytes"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// FuzzNewManifestFromReader feeds arbitrary bytes to the manifest parser.
|
|
// `make test` runs it on the seed corpus in
|
|
// testdata/fuzz/FuzzNewManifestFromReader; `make fuzz` searches for new
|
|
// inputs.
|
|
//
|
|
// For every input the parser must return a manifest or an error, not both
|
|
// and not neither, and must not allocate more than a fixed multiple of its
|
|
// input and of the decompressed data it may read, plus room for the
|
|
// decoder's window buffers. A panic or a hang fails the test on its own.
|
|
func FuzzNewManifestFromReader(f *testing.F) {
|
|
// A signed manifest makes the parser write the key and signature to a
|
|
// temporary directory and run gpg on them. With gpg off the PATH and
|
|
// temporary files kept in the test's own directory, no process is
|
|
// started and nothing is written elsewhere; such input ends in an
|
|
// error instead.
|
|
f.Setenv("PATH", "")
|
|
f.Setenv("TMPDIR", f.TempDir())
|
|
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
var before, after runtime.MemStats
|
|
|
|
runtime.ReadMemStats(&before)
|
|
|
|
m, err := NewManifestFromReader(bytes.NewReader(data))
|
|
|
|
runtime.ReadMemStats(&after)
|
|
|
|
if (m == nil) == (err == nil) {
|
|
t.Fatalf("got manifest %p and error %v, want exactly one", m, err)
|
|
}
|
|
|
|
// The parser reads at most the declared size plus one byte of
|
|
// decompressed data, and never more than MaxDecompressedSize.
|
|
decompressed := uint64(MaxDecompressedSize)
|
|
|
|
outer := new(MFFileOuter)
|
|
if validateMagic(data) &&
|
|
proto.Unmarshal(data[len(MAGIC):], outer) == nil {
|
|
size := outer.GetSize()
|
|
if size > 0 && size < MaxDecompressedSize {
|
|
decompressed = uint64(size) + 1
|
|
}
|
|
}
|
|
|
|
// It also keeps a few copies of its input. Buffers grow by
|
|
// copying, so reaching those sizes allocates a few times them in
|
|
// total: sixteen times the input and the decompressed data leaves
|
|
// room for that.
|
|
//
|
|
// The decoder also sets aside a new buffer of one to two times the
|
|
// window for each frame that asks for a larger window than the
|
|
// frames before it, and refuses windows above zstdWindowSize. A
|
|
// frame that gives its content size instead of a window has that
|
|
// size as its window, refused above zstdWindowSize like any other.
|
|
// Frames asking for every window size up to that make it set aside
|
|
// about 16 times zstdWindowSize in total; 24 times leaves room.
|
|
//
|
|
// The seed whose frame claims 8 GiB fails if the decoder sets that
|
|
// size aside; the seed whose frames ask for ever larger windows
|
|
// fails if the decoder accepts windows of twice zstdWindowSize; the
|
|
// seed whose two frames together exceed MaxDecompressedSize fails
|
|
// if the decoder decodes them in full instead of stopping at the
|
|
// declared size.
|
|
limit := 16*(uint64(len(data))+decompressed) + 24*zstdWindowSize
|
|
|
|
allocated := after.TotalAlloc - before.TotalAlloc
|
|
if allocated > limit {
|
|
t.Fatalf("allocated %d bytes for %d bytes of input, limit %d",
|
|
allocated, len(data), limit)
|
|
}
|
|
})
|
|
}
|