Skip to content

Commit f3324d1

Browse files
committed
Add some fuzz tests
1 parent c46c203 commit f3324d1

File tree

15 files changed

+204
-48
lines changed

15 files changed

+204
-48
lines changed

‎helpers.go‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,19 @@ type float64Provider interface {
117117
}
118118

119119
func (vc) convertAPEXToFNumber(byteOrder binary.ByteOrder, v any) any {
120-
r := v.(float64Provider)
120+
r, ok := v.(float64Provider)
121+
if !ok {
122+
return 0
123+
}
121124
f := r.Float64()
122125
return math.Pow(2, f/2)
123126
}
124127

125128
func (vc) convertAPEXToSeconds(byteOrder binary.ByteOrder, v any) any {
126-
r := v.(float64Provider)
129+
r, ok := v.(float64Provider)
130+
if !ok {
131+
return 0
132+
}
127133
f := r.Float64()
128134
f = 1 / math.Pow(2, f)
129135
return f

‎imagedecoder_png.go‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,15 @@ func (e *imageDecoderPNG) decode() error {
6262
if bytes.Equal(keyword, pngRawProfileTypeIPTC) {
6363
if sources.Has(IPTC) {
6464
sources = sources.Remove(IPTC)
65-
data := decompressZTXt(e.readBytesVolatile(int(chunkLength - zTXtKeywordLength)))
65+
data, err := decompressZTXt(e.readBytesVolatile(int(chunkLength - zTXtKeywordLength)))
66+
if err != nil {
67+
return newInvalidFormatError(fmt.Errorf("decompressing zTXt: %w", err))
68+
}
6669
// ImageMagick has different headers, so make this smarter. TODO1
6770
data = data[23:] // Skip the header bytes.
6871
data = bytes.ReplaceAll(data, []byte("\n"), []byte(""))
6972
d := make([]byte, hex.DecodedLen(len(data)))
70-
_, err := hex.Decode(d, data)
73+
_, err = hex.Decode(d, data)
7174
if err != nil {
7275
return fmt.Errorf("decoding hex: %w", err)
7376
}
@@ -96,21 +99,18 @@ func (e *imageDecoderPNG) decode() error {
9699
}
97100

98101
// TODO1 get rid of the panics.
99-
func decompressZTXt(data []byte) []byte {
102+
func decompressZTXt(data []byte) ([]byte, error) {
100103
// The first byte after the null indicates the compression method, for which only deflate is currently defined (method zero).
101104
compressionMethod := data[1]
102105
if compressionMethod != 0 {
103-
panic(fmt.Errorf("unknown PNG compression method %v", compressionMethod))
106+
return nil, fmt.Errorf("unknown PNG compression method %v", compressionMethod)
104107
}
105108
b := bytes.NewReader(data[2:])
106109
z, err := zlib.NewReader(b)
107110
if err != nil {
108-
panic(err)
111+
return nil, err
109112
}
110113
defer z.Close()
111114
p, err := io.ReadAll(z)
112-
if err != nil {
113-
panic(err)
114-
}
115-
return p
115+
return p, err
116116
}

‎imagedecoder_webp.go‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package imagemeta
22

33
import (
44
"errors"
5+
"strings"
56
)
67

78
var (
@@ -26,7 +27,36 @@ type InvalidFormatError struct {
2627
}
2728

2829
func (e *InvalidFormatError) Error() string {
29-
return e.Err.Error()
30+
return "invalid format: " + e.Err.Error()
31+
}
32+
33+
// Is reports whether the target error is an InvalidFormatError.
34+
func (e *InvalidFormatError) Is(target error) bool {
35+
_, ok := target.(*InvalidFormatError)
36+
return ok
37+
}
38+
39+
func newInvalidFormatErrorFromString(s string) error {
40+
return &InvalidFormatError{errors.New(s)}
41+
}
42+
43+
func newInvalidFormatError(err error) error {
44+
return &InvalidFormatError{err}
45+
}
46+
47+
// These error situations comes from the Go Fuzz modifying the input data to trigger panics.
48+
// We want to separate panics that we can do something about and "invalid format" errors.
49+
var invalidFormatErrorStrings = []string{
50+
"unexpected EOF",
51+
}
52+
53+
func isInvalidFormatErrorCandidate(err error) bool {
54+
for _, s := range invalidFormatErrorStrings {
55+
if strings.Contains(err.Error(), s) {
56+
return true
57+
}
58+
}
59+
return false
3060
}
3161

3262
type baseStreamingDecoder struct {

‎imagemeta.go‎

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,39 @@ const (
4444

4545
// Decode reads EXIF and IPTC metadata from r and returns a Meta struct.
4646
func Decode(opts Options) (err error) {
47+
var base *baseStreamingDecoder
48+
49+
defer func() {
50+
if r := recover(); r != nil {
51+
if errp := r.(error); errp != nil {
52+
if isInvalidFormatErrorCandidate(errp) {
53+
err = newInvalidFormatError(errp)
54+
} else if errp != errStop {
55+
panic(errp)
56+
}
57+
}
58+
}
59+
60+
if err == nil {
61+
if base != nil {
62+
err = base.streamErr()
63+
}
64+
}
65+
66+
if err == nil {
67+
return
68+
}
69+
70+
if err == io.EOF {
71+
err = nil
72+
return
73+
}
74+
75+
if isInvalidFormatErrorCandidate(err) {
76+
err = newInvalidFormatError(err)
77+
}
78+
}()
79+
4780
if opts.R == nil {
4881
return fmt.Errorf("no reader provided")
4982
}
@@ -97,28 +130,11 @@ func Decode(opts Options) (err error) {
97130
byteOrder: binary.BigEndian,
98131
}
99132

100-
base := &baseStreamingDecoder{
133+
base = &baseStreamingDecoder{
101134
streamReader: br,
102135
opts: opts,
103136
}
104137

105-
defer func() {
106-
if r := recover(); r != nil {
107-
if r != errStop {
108-
panic(r)
109-
}
110-
111-
if err == nil {
112-
err = base.streamErr()
113-
}
114-
115-
if err == io.EOF {
116-
err = nil
117-
}
118-
119-
}
120-
}()
121-
122138
var dec decoder
123139

124140
switch opts.ImageFormat {
@@ -134,18 +150,12 @@ func Decode(opts Options) (err error) {
134150
}
135151

136152
err = dec.decode()
137-
138-
if err == ErrStopWalking {
139-
return nil
140-
}
141-
142153
if err != nil {
143-
if err == io.EOF {
154+
if err == ErrStopWalking {
144155
return nil
145156
}
146-
return err
147157
}
148-
return nil
158+
return
149159
}
150160

151161
// HandleTagFunc is the function that is called for each tag.

‎imagemeta_fuzz._test.go‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package imagemeta_test
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/bep/imagemeta"
10+
)
11+
12+
func FuzzDecodeJPG(f *testing.F) {
13+
filenames := []string{"sunrise.jpg", "goexif/geodegrees_as_string.jpg", "metadata_demo_exif_only.jpg", "metadata_demo_iim_and_xmp_only.jpg"}
14+
for _, filename := range filenames {
15+
f.Add(readTestDataFileAll(f, filename))
16+
}
17+
18+
f.Fuzz(func(t *testing.T, imageBytes []byte) {
19+
fuzzDecodeBytes(t, imageBytes, imagemeta.JPEG)
20+
})
21+
}
22+
23+
func FuzzDecodeWebP(f *testing.F) {
24+
filenames := []string{"sunrise.webp"}
25+
26+
for _, filename := range filenames {
27+
f.Add(readTestDataFileAll(f, filename))
28+
}
29+
30+
f.Fuzz(func(t *testing.T, imageBytes []byte) {
31+
fuzzDecodeBytes(t, imageBytes, imagemeta.WebP)
32+
})
33+
}
34+
35+
func FuzzDecodePNG(f *testing.F) {
36+
filenames := []string{"sunrise.png", "metadata-extractor-images/png/issue614.png"}
37+
38+
for _, filename := range filenames {
39+
f.Add(readTestDataFileAll(f, filename))
40+
}
41+
42+
f.Fuzz(func(t *testing.T, imageBytes []byte) {
43+
fuzzDecodeBytes(t, imageBytes, imagemeta.PNG)
44+
})
45+
}
46+
47+
func FuzzDecodeTIFF(f *testing.F) {
48+
filenames := []string{"sunrise.tif"}
49+
50+
for _, filename := range filenames {
51+
f.Add(readTestDataFileAll(f, filename))
52+
}
53+
54+
f.Fuzz(func(t *testing.T, imageBytes []byte) {
55+
fuzzDecodeBytes(t, imageBytes, imagemeta.TIFF)
56+
})
57+
}
58+
59+
func fuzzDecodeBytes(t *testing.T, imageBytes []byte, f imagemeta.ImageFormat) error {
60+
r := bytes.NewReader(imageBytes)
61+
err := imagemeta.Decode(imagemeta.Options{R: r, ImageFormat: f, Sources: imagemeta.EXIF | imagemeta.IPTC | imagemeta.XMP})
62+
if err != nil {
63+
if !imagemeta.IsInvalidFormat(err) {
64+
t.Fatalf("unknown error in Decode: %v %T", err, err)
65+
}
66+
}
67+
return nil
68+
}
69+
70+
func readTestDataFileAll(t testing.TB, filename string) []byte {
71+
t.Helper()
72+
b, err := os.ReadFile(filepath.Join("testdata", filename))
73+
if err != nil {
74+
t.Fatalf("failed to read file %q: %v", filename, err)
75+
}
76+
return b
77+
}

‎imagemeta_test.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,9 @@ func withGolden(t testing.TB, sources imagemeta.Source) {
742742
if strings.HasPrefix(path, "corrupt") {
743743
return nil
744744
}
745+
if strings.HasPrefix(path, "fuzz") {
746+
return nil
747+
}
745748
if goldenSkip[filepath.ToSlash(path)] {
746749
return nil
747750
}

‎io.go‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ var bytesAndReaderPool = &sync.Pool{
2424

2525
func getBytesAndReader(length int) *bytesAndReader {
2626
b := bytesAndReaderPool.Get().(*bytesAndReader)
27-
if cap(b.b) < length {
27+
if length > len(b.b) {
2828
b.b = make([]byte, length)
2929
}
3030
b.b = b.b[:length]
@@ -76,9 +76,27 @@ type streamReader struct {
7676
readerOffset int64
7777
}
7878

79+
var noopCloser closerFunc = func() error {
80+
return nil
81+
}
82+
7983
// bufferedReader reads length bytes from the stream and returns a ReaderCloser.
8084
// It's important to call Close on the ReaderCloser when done.
8185
func (e *streamReader) bufferedReader(length int64) (readerCloser, error) {
86+
if length == 0 {
87+
return struct {
88+
io.ReadSeeker
89+
io.Closer
90+
}{
91+
bytes.NewReader(nil),
92+
noopCloser,
93+
}, nil
94+
}
95+
96+
if length < 0 {
97+
return nil, newInvalidFormatErrorFromString("negative length")
98+
}
99+
82100
br := getBytesAndReader(int(length))
83101

84102
_, err := io.ReadFull(e.r, br.b)

‎metadecoder_iptc.go‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ var (
9595
b := v.([]byte)
9696
s := resolveCodedCharacterSet(b)
9797
if s == "" {
98-
return UTF8
98+
return characterSetUTF8
9999
}
100100
return s
101101
},
@@ -273,7 +273,7 @@ func (e *metaDecoderIPTC) decodeRecord(stringSlices map[iptcField][]string) erro
273273
switch recordDef.Format {
274274
case "string":
275275
v = e.readBytesVolatile(int(recordSize))
276-
if e.charset == "" || e.charset == ISO88591 {
276+
if e.charset == "" || e.charset == characterSetISO88591 {
277277
v, _ = e.iso88591CharsetDecoder.Bytes(v.([]byte))
278278
}
279279
case "uint32":
@@ -375,8 +375,8 @@ func init() {
375375
}
376376

377377
const (
378-
UTF8 = "UTF-8"
379-
ISO88591 = "ISO-8859-1"
378+
characterSetUTF8 = "UTF-8"
379+
characterSetISO88591 = "ISO-8859-1"
380380
)
381381

382382
// resolveCodedCharacterSet resolves the coded character set from the IPTC data
@@ -392,19 +392,19 @@ func resolveCodedCharacterSet(b []byte) string {
392392
)
393393

394394
if len(b) > 2 && b[0] == esc && b[1] == percent && b[2] == latinCapitalG {
395-
return UTF8
395+
return characterSetUTF8
396396
}
397397

398398
if len(b) > 2 && b[0] == esc && b[1] == dot && b[2] == latinCapitalA {
399-
return ISO88591
399+
return characterSetISO88591
400400
}
401401

402402
if len(b) > 3 && b[0] == esc && (b[1] == dot || b[2] == dot || b[3] == dot) && b[4] == latinCapitalA {
403-
return ISO88591
403+
return characterSetISO88591
404404
}
405405

406406
if len(b) > 2 && b[0] == esc && b[1] == minus && b[2] == latinCapitalA {
407-
return ISO88591
407+
return characterSetISO88591
408408
}
409409

410410
return ""

‎metadecoder_xmp.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func decodeXMP(r io.Reader, opts Options) error {
4040

4141
var meta xmpmeta
4242
if err := xml.NewDecoder(r).Decode(&meta); err != nil {
43-
return fmt.Errorf("decoding XMP: %w", err)
43+
return newInvalidFormatError(fmt.Errorf("decoding XMP: %w", err))
4444
}
4545

4646
for _, attr := range meta.RDF.Description.Attrs {

‎testdata/fuzz/FuzzDecodeJPG/1ccacf70a7af5b48‎

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)