diff --git a/.gitignore b/.gitignore index 15add232b50..5500ec0372f 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,6 @@ Tests/images/jpeg2000 Tests/images/msp Tests/images/picins Tests/images/sunraster + +# pyinstaller +*.spec diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh new file mode 100755 index 00000000000..513136fffbd --- /dev/null +++ b/Tests/oss-fuzz/build.sh @@ -0,0 +1,48 @@ +#!/bin/bash -eu +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +python3 setup.py build --build-base=/tmp/build install + +# Build fuzzers in $OUT. +for fuzzer in $(find $SRC -name 'fuzz_*.py'); do + fuzzer_basename=$(basename -s .py $fuzzer) + fuzzer_package=${fuzzer_basename}.pkg + pyinstaller \ + --add-binary /usr/local/lib/libjpeg.so.9:. \ + --add-binary /usr/local/lib/libfreetype.so.6:. \ + --add-binary /usr/local/lib/liblcms2.so.2:. \ + --add-binary /usr/local/lib/libopenjp2.so.7:. \ + --add-binary /usr/local/lib/libpng16.so.16:. \ + --add-binary /usr/local/lib/libtiff.so.5:. \ + --add-binary /usr/local/lib/libwebp.so.7:. \ + --add-binary /usr/local/lib/libwebpdemux.so.2:. \ + --add-binary /usr/local/lib/libwebpmux.so.3:. \ + --add-binary /usr/local/lib/libxcb.so.1:. \ + --distpath $OUT --onefile --name $fuzzer_package $fuzzer + + # Create execution wrapper. + echo "#!/bin/sh +# LLVMFuzzerTestOneInput for fuzzer detection. +this_dir=\$(dirname \"\$0\") +LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \ +ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \ +\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename + chmod u+x $OUT/$fuzzer_basename +done + +find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@ +find Tests/fonts -print | zip -q $OUT/fuzz_font_seed_corpus.zip -@ diff --git a/Tests/oss-fuzz/build_dictionaries.sh b/Tests/oss-fuzz/build_dictionaries.sh new file mode 100755 index 00000000000..9aae56ca8d1 --- /dev/null +++ b/Tests/oss-fuzz/build_dictionaries.sh @@ -0,0 +1,33 @@ +#!/bin/bash -eu +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# Generate image dictionaries here for each of the fuzzers and put them in the +# $OUT directory, named for the fuzzer + +git clone --depth 1 https://github.com/google/fuzzing +cat fuzzing/dictionaries/bmp.dict \ + fuzzing/dictionaries/dds.dict \ + fuzzing/dictionaries/gif.dict \ + fuzzing/dictionaries/icns.dict \ + fuzzing/dictionaries/jpeg.dict \ + fuzzing/dictionaries/jpeg2000.dict \ + fuzzing/dictionaries/pbm.dict \ + fuzzing/dictionaries/png.dict \ + fuzzing/dictionaries/psd.dict \ + fuzzing/dictionaries/tiff.dict \ + fuzzing/dictionaries/webp.dict \ + > $OUT/fuzz_pillow.dict diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py new file mode 100755 index 00000000000..bdfda7a13aa --- /dev/null +++ b/Tests/oss-fuzz/fuzz_font.py @@ -0,0 +1,40 @@ +#!/usr/bin/python3 + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import atheris_no_libfuzzer as atheris +import fuzzers + + +def TestOneInput(data): + try: + fuzzers.fuzz_font(data) + except Exception: + # We're catching all exceptions because Pillow's exceptions are + # directly inheriting from Exception. + return + return + + +def main(): + fuzzers.enable_decompressionbomb_error() + atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 894068f6317..d816d535f8f 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -14,21 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import io import sys -import warnings import atheris_no_libfuzzer as atheris - -from PIL import Image, ImageFile, ImageFilter +import fuzzers def TestOneInput(data): try: - with Image.open(io.BytesIO(data)) as im: - im.rotate(45) - im.filter(ImageFilter.DETAIL) - im.save(io.BytesIO(), "BMP") + fuzzers.fuzz_image(data) except Exception: # We're catching all exceptions because Pillow's exceptions are # directly inheriting from Exception. @@ -37,9 +31,7 @@ def TestOneInput(data): def main(): - ImageFile.LOAD_TRUNCATED_IMAGES = True - warnings.filterwarnings("ignore") - warnings.simplefilter("error", Image.DecompressionBombWarning) + fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py new file mode 100644 index 00000000000..1e7a4e27df1 --- /dev/null +++ b/Tests/oss-fuzz/fuzzers.py @@ -0,0 +1,36 @@ +import io +import warnings + +from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont + + +def enable_decompressionbomb_error(): + ImageFile.LOAD_TRUNCATED_IMAGES = True + warnings.filterwarnings("ignore") + warnings.simplefilter("error", Image.DecompressionBombWarning) + + +def fuzz_image(data): + # This will fail on some images in the corpus, as we have many + # invalid images in the test suite. + with Image.open(io.BytesIO(data)) as im: + im.rotate(45) + im.filter(ImageFilter.DETAIL) + im.save(io.BytesIO(), "BMP") + + +def fuzz_font(data): + wrapper = io.BytesIO(data) + try: + font = ImageFont.truetype(wrapper) + except OSError: + # Catch pcf/pilfonts/random garbage here. They return + # different font objects. + return + + font.getsize_multiline("ABC\nAaaa") + font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py new file mode 100644 index 00000000000..a243c026040 --- /dev/null +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -0,0 +1,53 @@ +import subprocess +import sys + +import fuzzers +import pytest + +from PIL import Image + +if sys.platform.startswith("win32"): + pytest.skip("Fuzzer is linux only", allow_module_level=True) + + +@pytest.mark.parametrize( + "path", + subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), +) +def test_fuzz_images(path): + fuzzers.enable_decompressionbomb_error() + try: + with open(path, "rb") as f: + fuzzers.fuzz_image(f.read()) + assert True + except ( + OSError, + SyntaxError, + MemoryError, + ValueError, + NotImplementedError, + OverflowError, + ): + # Known exceptions that are through from Pillow + assert True + except ( + Image.DecompressionBombError, + Image.DecompressionBombWarning, + Image.UnidentifiedImageError, + ): + # Known Image.* exceptions + assert True + + +@pytest.mark.parametrize( + "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") +) +def test_fuzz_fonts(path): + if not path: + return + with open(path, "rb") as f: + try: + fuzzers.fuzz_font(f.read()) + except (Image.DecompressionBombError, Image.DecompressionBombWarning): + pass + assert True