pkg/container: harden the CAR file round-trip with fuzzing

This commit is contained in:
Michael Muré
2024-10-09 18:38:35 +02:00
parent 2a51d61b46
commit 100a510097
3 changed files with 45 additions and 9 deletions

View File

@@ -37,7 +37,7 @@ type carBlock struct {
// writeCar writes a CARv1 file containing the blocks from the iterator. // writeCar writes a CARv1 file containing the blocks from the iterator.
// If no roots are provided, a single EmptyCid is used as root to make the file // If no roots are provided, a single EmptyCid is used as root to make the file
// spec compliant. // spec compliant.
func writeCar(w io.Writer, roots []cid.Cid, blocks iter.Seq[carBlock]) error { func writeCar(w io.Writer, roots []cid.Cid, blocks iter.Seq2[carBlock, error]) error {
if len(roots) == 0 { if len(roots) == 0 {
roots = []cid.Cid{EmptyCid} roots = []cid.Cid{EmptyCid}
} }
@@ -54,7 +54,10 @@ func writeCar(w io.Writer, roots []cid.Cid, blocks iter.Seq[carBlock]) error {
return err return err
} }
for block := range blocks { for block, err := range blocks {
if err != nil {
return err
}
err = ldWrite(w, block.c.Bytes(), block.data) err = ldWrite(w, block.c.Bytes(), block.data)
if err != nil { if err != nil {
return err return err
@@ -144,6 +147,9 @@ func ldRead(r *bufio.Reader) ([]byte, error) {
} }
return nil, err return nil, err
} }
if l == 0 {
return nil, fmt.Errorf("invalid zero size section")
}
if l > uint64(maxAllowedSectionSize) { // Don't OOM if l > uint64(maxAllowedSectionSize) { // Don't OOM
return nil, fmt.Errorf("malformed car; header is bigger than MaxAllowedSectionSize") return nil, fmt.Errorf("malformed car; header is bigger than MaxAllowedSectionSize")
@@ -151,6 +157,10 @@ func ldRead(r *bufio.Reader) ([]byte, error) {
buf := make([]byte, l) buf := make([]byte, l)
if _, err := io.ReadFull(r, buf); err != nil { if _, err := io.ReadFull(r, buf); err != nil {
if err == io.EOF {
// we should be able to read the promised bytes, this is not normal
return nil, io.ErrUnexpectedEOF
}
return nil, err return nil, err
} }

View File

@@ -26,9 +26,9 @@ func TestCarRoundTrip(t *testing.T) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
err = writeCar(buf, roots, func(yield func(carBlock) bool) { err = writeCar(buf, roots, func(yield func(carBlock, error) bool) {
for _, blk := range blks { for _, blk := range blks {
if !yield(blk) { if !yield(blk, nil) {
return return
} }
} }
@@ -39,14 +39,40 @@ func TestCarRoundTrip(t *testing.T) {
require.Equal(t, original, buf.Bytes()) require.Equal(t, original, buf.Bytes())
} }
func FuzzCarRead(f *testing.F) { func FuzzCarRoundTrip(f *testing.F) {
example, err := os.ReadFile("testdata/sample-v1.car") example, err := os.ReadFile("testdata/sample-v1.car")
require.NoError(f, err) require.NoError(f, err)
f.Add(example) f.Add(example)
f.Fuzz(func(t *testing.T, data []byte) { f.Fuzz(func(t *testing.T, data []byte) {
_, _, _ = readCar(bytes.NewReader(data)) roots, blocksIter, err := readCar(bytes.NewReader(data))
// only looking for panics if err != nil {
// skip invalid binary
t.Skip()
}
// reading all the blocks, which force reading and verifying the full file
var blocks []carBlock
for block, err := range blocksIter {
if err != nil {
// error reading, invalid data
t.Skip()
}
blocks = append(blocks, block)
}
var buf bytes.Buffer
err = writeCar(&buf, roots, func(yield func(carBlock, error) bool) {
for _, blk := range blocks {
if !yield(blk, nil) {
return
}
}
})
require.NoError(t, err)
// test if the round-trip produce a byte-equal CAR
require.Equal(t, data, buf.Bytes())
}) })
} }

View File

@@ -27,9 +27,9 @@ func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
} }
func (ctn Writer) ToCar(w io.Writer) error { func (ctn Writer) ToCar(w io.Writer) error {
return writeCar(w, nil, func(yield func(carBlock) bool) { return writeCar(w, nil, func(yield func(carBlock, error) bool) {
for c, bytes := range ctn { for c, bytes := range ctn {
if !yield(carBlock{c: c, data: bytes}) { if !yield(carBlock{c: c, data: bytes}, nil) {
return return
} }
} }