package xfer
import (
"bytes"
"compress/zlib"
"context"
"encoding/binary"
"net/http"
"net/http/httptest"
"runtime"
"strings"
"testing"
)
// zeros is a container of n compressed zero bytes claiming the length
// claim: a few hundred KiB on the wire for every 100 MiB inflated.
func zeros(n int, claim uint32) []byte {
var buf bytes.Buffer
var head [4]byte
binary.BigEndian.PutUint32(head[:], claim)
buf.Write(head[:])
zw, _ := zlib.NewWriterLevel(&buf, zlib.BestCompression)
chunk := make([]byte, 1<<20)
for i := 0; i < n>>20; i++ {
zw.Write(chunk)
}
zw.Close()
return buf.Bytes()
}
// heapGrowth runs f and reports how far the heap grew.
func heapGrowth(f func()) uint64 {
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
f()
runtime.ReadMemStats(&after)
return after.TotalAlloc - before.TotalAlloc
}
// A container is inflated no further than its reader's limit, whatever
// length it claims: a claimed length over the limit is refused before
// any inflating, and a lying smaller one cannot run past the limit.
func TestContainerInflateIsBounded(t *testing.T) {
bomb := zeros(64<<20, 0xFFFFFFFF)
var ok bool
grew := heapGrowth(func() { _, ok = openContainer(bomb, 1<<20) })
if ok || grew > 8<<20 {
t.Fatalf("FAIL: a container claiming 4 GiB against a 1 MiB limit: ok %v, heap grew %d", ok, grew)
}
t.Logf("ok: a claimed length over the limit is refused (heap grew %d bytes)", grew)
liar := zeros(64<<20, 1<<20)
grew = heapGrowth(func() { _, ok = openContainer(liar, 1<<20) })
if ok || grew > 8<<20 {
t.Fatalf("FAIL: 64 MiB under a 1 MiB claim: ok %v, heap grew %d", ok, grew)
}
t.Logf("ok: a body longer than its claim stops at the limit (heap grew %d bytes)", grew)
body, ok := openContainer(encodeContainer([]byte("pragma x\n")), 1<<20)
if !ok || string(body) != "pragma x\n" {
t.Fatalf("FAIL: an ordinary container: %q %v", body, ok)
}
t.Log("ok: an ordinary container opens")
}
// The sync front decodes an anonymous request before any gate; a small
// request that inflates to gigabytes must cost it no more than the
// request cap.
func TestFrontDoesNotInflateABomb(t *testing.T) {
f := New()
defer f.Close()
f.SetAuth(&mapAuth{users: map[string]string{}})
bomb := zeros(300<<20, 0xFFFFFFFF)
grew := heapGrowth(func() {
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/r/nope", bytes.NewReader(bomb))
req.SetPathValue("repo", "nope")
f.ServeHTTP(rec, req)
})
if grew > 64<<20 {
t.Fatalf("FAIL: a %d-byte request made the front allocate %d bytes", len(bomb), grew)
}
t.Logf("ok: a %d-byte request claiming 4 GiB cost the front %d bytes", len(bomb), grew)
}
// Exchange refuses a reply that inflates past its MaxReply.
func TestUVExchangeRefusesABomb(t *testing.T) {
bomb := zeros(64<<20, 64<<20)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-fossil")
w.Write(bomb)
}))
defer srv.Close()
var err error
grew := heapGrowth(func() {
_, err = UVUpstream{URL: srv.URL, MaxReply: 1 << 20}.Exchange(context.Background(), "uvgimme a\n")
})
if err == nil || grew > 16<<20 {
t.Fatalf("FAIL: a reply inflating to 64 MiB under a 1 MiB cap: %v, heap grew %d", err, grew)
}
t.Logf("ok: refused (%v), heap grew %d bytes", strings.SplitN(err.Error(), ";", 2)[0], grew)
}