diff options
| author | 2026-06-23 19:59:26 +0000 | |
|---|---|---|
| committer | 2026-06-24 04:29:11 +0000 | |
| commit | 6fcbdbe96383d861a37058e49f4c69192bc58147 (patch) | |
| tree | 538aa8ca3a5dec199aab455395812aa475062155 | |
| parent | object/store: Add context to WritePack (diff) | |
object/store{,/packed}: Ingest allocation guard
| -rw-r--r-- | object/store/packed/internal/ingest/resolve.go | 6 | ||||
| -rw-r--r-- | object/store/packed/internal/ingest/scan.go | 6 | ||||
| -rw-r--r-- | object/store/packed/internal/ingest/writepack_test.go | 55 | ||||
| -rw-r--r-- | object/store/writer.go | 11 |
4 files changed, 78 insertions, 0 deletions
diff --git a/object/store/packed/internal/ingest/resolve.go b/object/store/packed/internal/ingest/resolve.go index 4e2adc13..ac040aca 100644 --- a/object/store/packed/internal/ingest/resolve.go +++ b/object/store/packed/internal/ingest/resolve.go @@ -10,6 +10,7 @@ import ( "lindenii.org/go/furgit/internal/progress" "lindenii.org/go/furgit/object/header" "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store" "lindenii.org/go/furgit/object/typ" ) @@ -247,6 +248,11 @@ func (ingestion *ingestion) applyDelta(index int, baseContent []byte) ([]byte, e return nil, fmt.Errorf("%w: entry at %d: %w", ErrMalformedPack, rec.offset, err) } + limit := ingestion.opts.MaxObjectSize + if limit > 0 && resultSize > uint64(limit) { + return nil, fmt.Errorf("%w: entry at %d: result size %d exceeds limit %d", store.ErrObjectTooLarge, rec.offset, resultSize, limit) + } + if baseSize != uint64(len(baseContent)) { return nil, fmt.Errorf("%w: entry at %d: delta base size mismatch", ErrMalformedPack, rec.offset) } diff --git a/object/store/packed/internal/ingest/scan.go b/object/store/packed/internal/ingest/scan.go index 31a47152..86cf7023 100644 --- a/object/store/packed/internal/ingest/scan.go +++ b/object/store/packed/internal/ingest/scan.go @@ -13,6 +13,7 @@ import ( "lindenii.org/go/furgit/internal/progress" "lindenii.org/go/furgit/object/header" "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store" "lindenii.org/go/lgo/intconv" ) @@ -388,6 +389,11 @@ func (ingestion *ingestion) scanHeader(start int) (record, error) { return rec, fmt.Errorf("%w: entry at %d: declared size overflows int: %w", ErrMalformedPack, start, err) } + limit := ingestion.opts.MaxObjectSize + if limit > 0 && declaredSize > limit { + return rec, fmt.Errorf("%w: entry at %d: declared size %d exceeds limit %d", store.ErrObjectTooLarge, start, declaredSize, limit) + } + rec.packedType = entryHeader.Type rec.declaredSize = declaredSize rec.headerLen = entryHeader.HeaderLen diff --git a/object/store/packed/internal/ingest/writepack_test.go b/object/store/packed/internal/ingest/writepack_test.go index b5a53a4f..b2f4d2b8 100644 --- a/object/store/packed/internal/ingest/writepack_test.go +++ b/object/store/packed/internal/ingest/writepack_test.go @@ -456,6 +456,61 @@ func TestWritePackContextCancelled(t *testing.T) { } } +// TestWritePackObjectTooLarge verifies that an object exceeding MaxObjectSize +// is rejected and no artifacts are published. +func TestWritePackObjectTooLarge(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, seeded := seedHistory(t, objectFormat) + + gitPrefix, err := repo.PackObjects(t, seeded.All(), testgit.PackObjectsOptions{ + RevIndex: false, + Revs: false, + Exclude: nil, + }) + if err != nil { + t.Fatalf("PackObjects: %v", err) + } + + stream, err := os.ReadFile(gitPrefix + ".pack") //nolint:gosec + if err != nil { + t.Fatalf("ReadFile pack: %v", err) + } + + dir := t.TempDir() + + root, err := os.OpenRoot(dir) + if err != nil { + t.Fatalf("OpenRoot: %v", err) + } + + t.Cleanup(func() { _ = root.Close() }) + + _, err = ingest.WritePack(t.Context(), root, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ + ThinBase: nil, + Progress: nil, + MaxObjectSize: 1, + }) + if !errors.Is(err, store.ErrObjectTooLarge) { + t.Fatalf("err = %v, want ErrObjectTooLarge", err) + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + + if len(entries) != 0 { + t.Fatalf("rejected ingestion left %d files behind", len(entries)) + } + }) + } +} + // seedHistory creates one repository with a seeded history. func seedHistory(t *testing.T, objectFormat id.ObjectFormat) (*testgit.Repo, testgit.Seeded) { t.Helper() diff --git a/object/store/writer.go b/object/store/writer.go index eeff071c..0437505d 100644 --- a/object/store/writer.go +++ b/object/store/writer.go @@ -13,6 +13,10 @@ import ( // ErrInvalidObject indicates a malformed object passed to a write. var ErrInvalidObject = errors.New("object/store: invalid object") +// ErrObjectTooLarge indicates that an object exceeds +// the size limit configured for the write. +var ErrObjectTooLarge = errors.New("object/store: object too large") + // ObjectWriter writes individual Git objects. type ObjectWriter interface { // WriteBytesFull writes one full serialized object byte slice as "type size\x00content". @@ -66,4 +70,11 @@ type PackWriteOptions struct { // // When nil, no progress output is emitted. Progress iowrap.WriteFlusher + + // MaxObjectSize rejects ingestion of any object + // whose declared inflated size or delta result size exceeds it, + // bounding the memory spent reconstructing a single object. + // + // Zero or negative means no limit. + MaxObjectSize int } |
