detached 5dde9c6

perf(snapshot): size the inflate buffer from the compressed length

commit
5dde9c6cf30a0db07607083feacf0b9b723cd613
author
brice · · 2026-10-08 20:07 UTC
parent
e0f6671
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +13 -9
M internal/snapshotcodec/object_compress.go +12 -8
@@ -15,10 +15,14 @@ import (
1515 // deterministic for one toolchain and level, so content addressing stays
1616 // stable between checkpoints of one build.
1717
18-// inflateInitialCap bounds the up-front allocation for one payload, so a
19-// corrupt declared length cannot allocate more than the stream produces.
18+// inflateInitialCap is the up-front allocation floor for one payload.
2019 const inflateInitialCap = 1 << 20
2120
21+// deflateMaxRatio bounds how many bytes one compressed byte can inflate to
22+// (deflate tops out near 1032:1), so a payload the stream can really produce
23+// is allocated once while a corrupt declared length stays bounded.
24+const deflateMaxRatio = 1032
25+
2226 var objectCompressorPool = sync.Pool{New: func() any {
2327 w, err := zlib.NewWriterLevel(io.Discard, zlib.BestSpeed)
2428 if err != nil {
@@ -58,7 +62,7 @@ func inflateObjectPayload(compressed []byte, size uint32) ([]byte, error) {
5862 return nil, invalid(err)
5963 }
6064 defer objectDecompressorPool.Put(r)
61- payload, err := readExact(r, int(size))
65+ payload, err := readExact(r, int(size), max(inflateInitialCap, len(compressed)*deflateMaxRatio))
6266 if err != nil {
6367 return nil, invalid(err)
6468 }
@@ -76,11 +80,11 @@ func inflateObjectPayload(compressed []byte, size uint32) ([]byte, error) {
7680 }
7781
7882 // readExact reads exactly size bytes. It doubles the buffer as data arrives,
79-// starting at inflateInitialCap and capping every step at size, so a corrupt
80-// declared length allocates at most about four times what the stream produces
81-// (about three times live at peak) and the returned payload has no slack.
82-func readExact(r io.Reader, size int) ([]byte, error) {
83- buf := make([]byte, min(size, inflateInitialCap))
83+// starting at initial and capping every step at size, so a corrupt declared
84+// length allocates at most about four times max(initial, what the stream
85+// produces) and the returned payload has no slack.
86+func readExact(r io.Reader, size, initial int) ([]byte, error) {
87+ buf := make([]byte, min(size, initial))
8488 for n := 0; ; {
8589 m, err := io.ReadFull(r, buf[n:])
8690 n += m
@@ -15,10 +15,14 @@ import (
15 // deterministic for one toolchain and level, so content addressing stays15 // deterministic for one toolchain and level, so content addressing stays
16 // stable between checkpoints of one build.16 // stable between checkpoints of one build.
17 17
18-// inflateInitialCap bounds the up-front allocation for one payload, so a18+// inflateInitialCap is the up-front allocation floor for one payload.
19-// corrupt declared length cannot allocate more than the stream produces.
20 const inflateInitialCap = 1 << 2019 const inflateInitialCap = 1 << 20
21 20
21+// deflateMaxRatio bounds how many bytes one compressed byte can inflate to
22+// (deflate tops out near 1032:1), so a payload the stream can really produce
23+// is allocated once while a corrupt declared length stays bounded.
24+const deflateMaxRatio = 1032
25+
22 var objectCompressorPool = sync.Pool{New: func() any {26 var objectCompressorPool = sync.Pool{New: func() any {
23 w, err := zlib.NewWriterLevel(io.Discard, zlib.BestSpeed)27 w, err := zlib.NewWriterLevel(io.Discard, zlib.BestSpeed)
24 if err != nil {28 if err != nil {
@@ -58,7 +62,7 @@ func inflateObjectPayload(compressed []byte, size uint32) ([]byte, error) {
58 return nil, invalid(err)62 return nil, invalid(err)
59 }63 }
60 defer objectDecompressorPool.Put(r)64 defer objectDecompressorPool.Put(r)
61- payload, err := readExact(r, int(size))65+ payload, err := readExact(r, int(size), max(inflateInitialCap, len(compressed)*deflateMaxRatio))
62 if err != nil {66 if err != nil {
63 return nil, invalid(err)67 return nil, invalid(err)
64 }68 }
@@ -76,11 +80,11 @@ func inflateObjectPayload(compressed []byte, size uint32) ([]byte, error) {
76 }80 }
77 81
78 // readExact reads exactly size bytes. It doubles the buffer as data arrives,82 // readExact reads exactly size bytes. It doubles the buffer as data arrives,
79-// starting at inflateInitialCap and capping every step at size, so a corrupt83+// starting at initial and capping every step at size, so a corrupt declared
80-// declared length allocates at most about four times what the stream produces84+// length allocates at most about four times max(initial, what the stream
81-// (about three times live at peak) and the returned payload has no slack.85+// produces) and the returned payload has no slack.
82-func readExact(r io.Reader, size int) ([]byte, error) {86+func readExact(r io.Reader, size, initial int) ([]byte, error) {
83- buf := make([]byte, min(size, inflateInitialCap))87+ buf := make([]byte, min(size, initial))
84 for n := 0; ; {88 for n := 0; ; {
85 m, err := io.ReadFull(r, buf[n:])89 m, err := io.ReadFull(r, buf[n:])
86 n += m90 n += m
M internal/snapshotcodec/object_compress_test.go +1 -1
@@ -79,7 +79,7 @@ func TestInflateObjectPayloadAllocation(t *testing.T) {
7979 }{
8080 {name: "small payload", size: 64 << 10, maxBytes: 64<<10 + 64<<10},
8181 {name: "payload at the initial cap", size: inflateInitialCap, maxBytes: inflateInitialCap + 64<<10},
82- {name: "payload above the initial cap", size: 3 << 20, maxBytes: 2*(3<<20) + 64<<10},
82+ {name: "payload above the initial cap", size: 3 << 20, maxBytes: 3<<20 + 64<<10},
8383 {name: "declared maximum on a small stream", size: 64 << 10, declared: maxObjectPayloadSize, maxBytes: 2*inflateInitialCap + 64<<10},
8484 } {
8585 t.Run(tc.name, func(t *testing.T) {
@@ -79,7 +79,7 @@ func TestInflateObjectPayloadAllocation(t *testing.T) {
79 }{79 }{
80 {name: "small payload", size: 64 << 10, maxBytes: 64<<10 + 64<<10},80 {name: "small payload", size: 64 << 10, maxBytes: 64<<10 + 64<<10},
81 {name: "payload at the initial cap", size: inflateInitialCap, maxBytes: inflateInitialCap + 64<<10},81 {name: "payload at the initial cap", size: inflateInitialCap, maxBytes: inflateInitialCap + 64<<10},
82- {name: "payload above the initial cap", size: 3 << 20, maxBytes: 2*(3<<20) + 64<<10},82+ {name: "payload above the initial cap", size: 3 << 20, maxBytes: 3<<20 + 64<<10},
83 {name: "declared maximum on a small stream", size: 64 << 10, declared: maxObjectPayloadSize, maxBytes: 2*inflateInitialCap + 64<<10},83 {name: "declared maximum on a small stream", size: 64 << 10, declared: maxObjectPayloadSize, maxBytes: 2*inflateInitialCap + 64<<10},
84 } {84 } {
85 t.Run(tc.name, func(t *testing.T) {85 t.Run(tc.name, func(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close