//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 up to about six times // them in total. Decoding the decompressed data takes up to // maxDecodedGrowth times its size for file entries, hashes, // timestamps and MIME types, and drops fields it does not know. // The strings and bytes it copies out of it, such as many one-byte // values in one hash, take up to about five times more under the // race detector, which pads every small copy to 16 bytes, and about // half that without it. Twenty times the input and the // decompressed data leaves room for all of 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; the seeds of empty file entries and of a file // entry of empty hashes fail if the parser decodes them. limit := 20*(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) } }) }