tiff: cap buffer growth to prevent OOM from malicious IFD offset A crafted 8-byte TIFF file with IFD offset 0xFFFFFFFF causes buffer.fill() to allocate ~4GB of memory when decoding via io.Reader (non-ReaderAt path), leading to an out-of-memory crash in any Go application that calls Decode or DecodeConfig on untrusted input. Read the data, and allocate the buffer, in chunks, to limit memory allocation to the size of the input file. References: https://issuetracker.google.com/issues/494365189 Fixes golang/go#78267 Change-Id: I514161af87fb3ad24180ec4bed61fa49f491e721 GitHub-Last-Rev: 8e6d97892cfbdea81fa9e9ec3e3872e4e452aea9 GitHub-Pull-Request: golang/image#25 Reviewed-on: https://go-review.googlesource.com/c/image/+/757660 Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org> Reviewed-by: Carlos Amedee <carlos@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/tiff/buffer.go b/tiff/buffer.go index d1801be..aaf1b06 100644 --- a/tiff/buffer.go +++ b/tiff/buffer.go
@@ -4,7 +4,10 @@ package tiff -import "io" +import ( + "io" + "slices" +) // buffer buffers an io.Reader to satisfy io.ReaderAt. type buffer struct { @@ -12,24 +15,19 @@ buf []byte } +const fillChunkSize = 10 << 20 // 10 MB + // fill reads data from b.r until the buffer contains at least end bytes. func (b *buffer) fill(end int) error { m := len(b.buf) - if end > m { - if end > cap(b.buf) { - newcap := 1024 - for newcap < end { - newcap *= 2 - } - newbuf := make([]byte, end, newcap) - copy(newbuf, b.buf) - b.buf = newbuf - } else { - b.buf = b.buf[:end] - } - if n, err := io.ReadFull(b.r, b.buf[m:end]); err != nil { - end = m + n - b.buf = b.buf[:end] + for m < end { + next := min(end-m, fillChunkSize) + b.buf = slices.Grow(b.buf, next) + b.buf = b.buf[:m+next] + n, err := io.ReadFull(b.r, b.buf[m:m+next]) + m += n + b.buf = b.buf[:m] + if err != nil { return err } } @@ -44,7 +42,8 @@ } err := b.fill(end) - return copy(p, b.buf[o:end]), err + end = min(end, len(b.buf)) + return copy(p, b.buf[min(o, end):end]), err } // Slice returns a slice of the underlying buffer. The slice contains
diff --git a/tiff/reader_test.go b/tiff/reader_test.go index 0e028f6..c34ac01 100644 --- a/tiff/reader_test.go +++ b/tiff/reader_test.go
@@ -593,3 +593,34 @@ b = enc.AppendUint32(b, 0) return b } + +// ioReader wraps an io.Reader to hide any io.ReaderAt implementation, +// forcing the tiff package to use the buffer code path. +type ioReader struct { + io.Reader +} + +// TestDecodeOOMIFDOffset tests that a TIFF with an IFD offset of 0xFFFFFFFF +// does not cause an out-of-memory panic in buffer.fill. +func TestDecodeOOMIFDOffset(t *testing.T) { + for _, endian := range []struct { + name string + header []byte + }{ + {"little-endian", []byte{'I', 'I', 42, 0, 0xff, 0xff, 0xff, 0xff}}, + {"big-endian", []byte{'M', 'M', 0, 42, 0xff, 0xff, 0xff, 0xff}}, + } { + t.Run(endian.name, func(t *testing.T) { + r := ioReader{bytes.NewReader(endian.header)} + _, err := Decode(r) + if err == nil { + t.Error("Decode with IFD offset 0xFFFFFFFF: got nil error, want non-nil") + } + r = ioReader{bytes.NewReader(endian.header)} + _, err = DecodeConfig(r) + if err == nil { + t.Error("DecodeConfig with IFD offset 0xFFFFFFFF: got nil error, want non-nil") + } + }) + } +}