Skip to content

Commit 3c3be60

Browse files
authored
Merge pull request #2137 from go-git/validate-v5
plumbing: format/packfile, cap delta chain depth in parser
2 parents a97d660 + 3fba897 commit 3c3be60

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package packfile
2+
3+
import (
4+
"bytes"
5+
"compress/zlib"
6+
"crypto/sha1"
7+
"encoding/binary"
8+
"io"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/go-git/go-git/v5/plumbing"
14+
gogitbinary "github.com/go-git/go-git/v5/utils/binary"
15+
)
16+
17+
func TestParserRejectsDeepDeltaChain(t *testing.T) {
18+
t.Parallel()
19+
20+
pack := buildLinearDeltaChainPack(t, maxDeltaChainDepth+1)
21+
scanner := NewScanner(bytes.NewReader(pack))
22+
parser, err := NewParser(scanner)
23+
require.NoError(t, err)
24+
25+
_, err = parser.Parse()
26+
require.ErrorIs(t, err, ErrMalformedPackFile)
27+
require.ErrorContains(t, err, "delta chain depth")
28+
}
29+
30+
func TestParserAcceptsMaxDepthDeltaChain(t *testing.T) {
31+
t.Parallel()
32+
33+
pack := buildLinearDeltaChainPack(t, maxDeltaChainDepth)
34+
scanner := NewScanner(bytes.NewReader(pack))
35+
parser, err := NewParser(scanner)
36+
require.NoError(t, err)
37+
38+
_, err = parser.Parse()
39+
require.NoError(t, err)
40+
}
41+
42+
// buildLinearDeltaChainPack returns a pack containing one base blob followed
43+
// by deltaCount OFS deltas, each referencing the immediately preceding object.
44+
// The resulting chain depth (counting only delta links) equals deltaCount.
45+
func buildLinearDeltaChainPack(t *testing.T, deltaCount int) []byte {
46+
t.Helper()
47+
48+
objects := make([]testPackObject, 0, deltaCount+1)
49+
objects = append(objects, testPackObject{
50+
typ: plumbing.BlobObject,
51+
content: []byte{0, 0},
52+
})
53+
for i := range deltaCount {
54+
content := []byte{byte(i + 1), byte((i + 1) >> 8)}
55+
delta := buildDelta(2, 2, insertOp(content))
56+
objects = append(objects, testPackObject{
57+
typ: plumbing.OFSDeltaObject,
58+
content: delta,
59+
offsetDeltaDistance: -1,
60+
})
61+
}
62+
return buildTestPack(t, objects...)
63+
}
64+
65+
type testPackObject struct {
66+
typ plumbing.ObjectType
67+
declaredSize int64
68+
content []byte
69+
reference plumbing.Hash
70+
offsetDeltaDistance int64 // for OFS deltas; -1 means "previous object"
71+
}
72+
73+
// buildTestPack assembles a pack with the given objects, returning the raw
74+
// pack bytes. Object offsets are not exposed because the only caller does
75+
// not need them.
76+
func buildTestPack(t *testing.T, objects ...testPackObject) []byte {
77+
t.Helper()
78+
79+
var body bytes.Buffer
80+
body.WriteString("PACK")
81+
require.NoError(t, binary.Write(&body, binary.BigEndian, uint32(2)))
82+
require.NoError(t, binary.Write(&body, binary.BigEndian, uint32(len(objects))))
83+
84+
offsets := make([]int64, 0, len(objects))
85+
for _, obj := range objects {
86+
offsets = append(offsets, int64(body.Len()))
87+
declaredSize := obj.declaredSize
88+
if declaredSize == 0 && len(obj.content) > 0 {
89+
declaredSize = int64(len(obj.content))
90+
}
91+
92+
writeTestObjectHeader(&body, obj.typ, declaredSize)
93+
switch obj.typ {
94+
case plumbing.REFDeltaObject:
95+
body.Write(obj.reference[:])
96+
case plumbing.OFSDeltaObject:
97+
distance := obj.offsetDeltaDistance
98+
if distance == -1 {
99+
// Reference the immediately preceding object.
100+
distance = offsets[len(offsets)-1] - offsets[len(offsets)-2]
101+
}
102+
require.NoError(t, gogitbinary.WriteVariableWidthInt(&body, distance))
103+
}
104+
body.Write(zlibCompress(t, obj.content))
105+
}
106+
107+
sum := sha1.Sum(body.Bytes())
108+
body.Write(sum[:])
109+
return body.Bytes()
110+
}
111+
112+
func writeTestObjectHeader(w io.ByteWriter, typ plumbing.ObjectType, size int64) {
113+
remaining := uint64(size)
114+
first := byte(typ)<<4 | byte(remaining&0x0f)
115+
remaining >>= 4
116+
if remaining > 0 {
117+
first |= 0x80
118+
}
119+
_ = w.WriteByte(first)
120+
121+
for remaining > 0 {
122+
next := byte(remaining & 0x7f)
123+
remaining >>= 7
124+
if remaining > 0 {
125+
next |= 0x80
126+
}
127+
_ = w.WriteByte(next)
128+
}
129+
}
130+
131+
func zlibCompress(t *testing.T, content []byte) []byte {
132+
t.Helper()
133+
134+
var compressed bytes.Buffer
135+
zw := zlib.NewWriter(&compressed)
136+
_, err := zw.Write(content)
137+
require.NoError(t, err)
138+
require.NoError(t, zw.Close())
139+
return compressed.Bytes()
140+
}

plumbing/format/packfile/parser.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const maxObjectPreallocBytes = 1 << 30 // 1 GiB
3838
// organically beyond this hint.
3939
const maxObjectsPrealloc = 1 << 16 // 64 Ki entries
4040

41+
// Match upstream Git's pack depth ceiling: pack-objects.h OE_DEPTH_BITS,
42+
// enforced in builtin/pack-objects.c as (1 << OE_DEPTH_BITS) - 1.
43+
const maxDeltaChainDepth = 4095
44+
4145
// growHint returns a non-negative int64 size, clamped to a sane upper bound,
4246
// suitable for passing to bytes.Buffer.Grow.
4347
func growHint(n int64) int {
@@ -353,6 +357,10 @@ func (p *Parser) resolveDeltas() error {
353357
defer sync.PutBytesBuffer(buf)
354358

355359
for _, obj := range p.oi {
360+
if err := checkDeltaChainDepth(obj); err != nil {
361+
return err
362+
}
363+
356364
buf.Reset()
357365
buf.Grow(growHint(obj.Length))
358366
err := p.get(obj, buf)
@@ -373,6 +381,9 @@ func (p *Parser) resolveDeltas() error {
373381
// create it once and reuse across all children.
374382
r := bytes.NewReader(buf.Bytes())
375383
for _, child := range obj.Children {
384+
if err := checkDeltaChainDepth(child); err != nil {
385+
return err
386+
}
376387
// Even though we are discarding the output, we still need to read it to
377388
// so that the scanner can advance to the next object, and the SHA1 can be
378389
// calculated.
@@ -392,6 +403,17 @@ func (p *Parser) resolveDeltas() error {
392403
return nil
393404
}
394405

406+
func checkDeltaChainDepth(o *objectInfo) error {
407+
var depth int
408+
for current := o; current != nil && current.DiskType.IsDelta(); current = current.Parent {
409+
depth++
410+
if depth > maxDeltaChainDepth {
411+
return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackFile, maxDeltaChainDepth)
412+
}
413+
}
414+
return nil
415+
}
416+
395417
func (p *Parser) resolveExternalRef(o *objectInfo) {
396418
if ref, ok := p.oiByHash[o.SHA1]; ok && ref.ExternalRef {
397419
p.oiByHash[o.SHA1] = o

0 commit comments

Comments
 (0)