Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More OSS-Fuzz support #5328

Merged
merged 18 commits into from Mar 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -88,3 +88,6 @@ Tests/images/jpeg2000
Tests/images/msp
Tests/images/picins
Tests/images/sunraster

# pyinstaller
*.spec
48 changes: 48 additions & 0 deletions 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 -@
33 changes: 33 additions & 0 deletions 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
40 changes: 40 additions & 0 deletions 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()
14 changes: 3 additions & 11 deletions Tests/oss-fuzz/fuzz_pillow.py
Expand Up @@ -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.
Expand All @@ -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()

Expand Down
36 changes: 36 additions & 0 deletions 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")
53 changes: 53 additions & 0 deletions 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pytest.skip("Fuzzer is linux only", allow_module_level=True)
pytest.skip("Fuzzer is macOS and Linux only", allow_module_level=True)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, technically, it's linux only, but the required bits don't fail on osx because it includes the find command.



@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
Comment on lines +43 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
)
def test_fuzz_fonts(path):
if not path:
return
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).strip().split(b"\n")
)
def test_fuzz_fonts(path):

with open(path, "rb") as f:
try:
fuzzers.fuzz_font(f.read())
except (Image.DecompressionBombError, Image.DecompressionBombWarning):
pass
assert True