Skip to content
quarry Artifact [2ad5c6db47]
Login

Artifact [2ad5c6db47]

Artifact 2ad5c6db470e8d71d8c106021de8df82a8e6d1179083d6ba23cf0f7e085ff5b8:


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)
}