diff --git a/Makefile b/Makefile index 53eaa0566..af3059f34 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,13 @@ sdist: test: pytest -qq +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + .PHONY: readme readme: python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html diff --git a/Tests/conftest.py b/Tests/conftest.py index dd37e7ce5..66da7593c 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -13,6 +13,11 @@ def pytest_report_header(config): def pytest_configure(config): + config.addinivalue_line( + "markers", + "pil_noop_mark: A conditional mark where nothing special happens", + ) + # We're marking some tests to ignore valgrind errors and XFAIL them. # Ensure that the mark is defined # even in cases where pytest-valgrind isn't installed diff --git a/Tests/helper.py b/Tests/helper.py index 59ba0dd15..8504993fb 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -173,6 +173,21 @@ def skip_unless_feature_version(feature, version_required, reason=None): return pytest.mark.skipif(version_available < version_required, reason=reason) +def mark_if_feature_version(mark, feature, version_blacklist, reason=None): + if not features.check(feature): + return pytest.mark.pil_noop_mark() + if reason is None: + reason = f"{feature} is {version_blacklist}" + version_required = parse_version(version_blacklist) + version_available = parse_version(features.version(feature)) + if ( + version_available.major == version_required.major + and version_available.minor == version_required.minor + ): + return mark(reason=reason) + return pytest.mark.pil_noop_mark() + + @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") class PillowLeakTestCase: # requires unix/macOS diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp new file mode 100644 index 000000000..94cc87db9 --- /dev/null +++ b/Tests/oss-fuzz/python.supp @@ -0,0 +1,16 @@ +{ + + Memcheck:Cond + ... + fun:encode_current_locale +} + + +{ + + Memcheck:Cond + fun:inflate + fun:ZIPDecode + fun:_TIFFReadEncodedTileAndAllocBuffer + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index a243c0260..2ac1a0d7f 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -2,12 +2,19 @@ import subprocess import sys import fuzzers +import packaging import pytest -from PIL import Image +from PIL import Image, features if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) +if features.check("libjpeg_turbo"): + version = packaging.version.parse(features.version("libjpeg_turbo")) + if version.major == 2 and version.minor == 0: + pytestmark = pytest.mark.valgrind_known_error( + reason="Known failing with libjpeg_turbo 2.0" + ) @pytest.mark.parametrize( diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 7caac34c3..1994a124c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -8,6 +8,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -64,7 +65,9 @@ def test_invalid_file(): EpsImagePlugin.EpsImageFile(invalid_file) -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 64f509a95..18837156a 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -24,6 +24,7 @@ from .helper import ( djpeg_available, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -116,7 +117,9 @@ class TestFileJpeg: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_icc(self, tmp_path): # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: @@ -156,7 +159,9 @@ class TestFileJpeg: test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK * 4 + 3) # large block - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_large_icc_meta(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than @@ -423,7 +428,9 @@ class TestFileJpeg: with Image.open(filename): pass - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -442,7 +449,9 @@ class TestFileJpeg: with pytest.raises(OSError): im.load() - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_qtables(self, tmp_path): def _n_qtables_helper(n, test_file): with Image.open(test_file) as im: @@ -726,7 +735,9 @@ class TestFileJpeg: # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_x_resolution(self, tmp_path): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -757,7 +768,9 @@ class TestFileJpeg: # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_photoshop(self): with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 22b641b5f..c0d09d6ce 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -17,6 +17,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -822,13 +823,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: @@ -839,13 +844,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/flower2.jpg") - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index c7839873a..005acdfdd 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser -from .helper import hopper +from .helper import hopper, mark_if_feature_version def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -85,7 +85,9 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_save_all(tmp_path): # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index bbf5f5772..a1234b46a 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -13,6 +13,7 @@ from .helper import ( hopper, is_big_endian, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -679,7 +680,9 @@ class TestFilePng: exif = reloaded._getexif() assert exif[274] == 1 - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index cb133e2c5..e6d6fc63f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import skip_unless_feature +from .helper import mark_if_feature_version, skip_unless_feature pytestmark = [ skip_unless_feature("webp"), @@ -41,7 +41,9 @@ def test_read_exif_metadata_without_prefix(): assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_exif_metadata(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() @@ -74,7 +76,9 @@ def test_read_icc_profile(): assert icc == expected_icc -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_icc_metadata(): file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() @@ -92,7 +96,9 @@ def test_write_icc_metadata(): assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_read_no_exif(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() diff --git a/Tests/test_image.py b/Tests/test_image.py index 30d093e15..82efefc1e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ from .helper import ( assert_not_all_same, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -662,7 +663,9 @@ class TestImage: assert not fp.closed - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_jpeg(self, tmp_path): with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 69449198e..8bf2ce916 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -4,7 +4,12 @@ import pytest from PIL import Image, ImageDraw -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + mark_if_feature_version, +) class TestImagingResampleVulnerability: @@ -455,7 +460,9 @@ class TestCoreResampleBox: tiled.paste(tile, (x0, y0)) return tiled - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiles(self): with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) @@ -466,7 +473,9 @@ class TestCoreResampleBox: tiled = self.resize_tiled(im, dst_size, *tiles) assert_image_similar(reference, tiled, 0.01) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6911ce460..dd140955d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -88,6 +88,7 @@ def test_no_resize(): assert im.size == (64, 64) +# valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") def test_DCT_scaling_edges(): # Make an image with red borders and size (N * 8) + 1 to cross DCT grid diff --git a/src/decode.c b/src/decode.c index a29c6a46e..dcd11eb5d 100644 --- a/src/decode.c +++ b/src/decode.c @@ -199,7 +199,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->bytes = (state->bits * state->xsize + 7) / 8; } /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } diff --git a/src/encode.c b/src/encode.c index f92ba62c2..c47255952 100644 --- a/src/encode.c +++ b/src/encode.c @@ -264,7 +264,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { } state->bytes = (state->bits * state->xsize + 7) / 8; /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index f086848e9..6af0f2eae 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -861,6 +861,10 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { state->state = J2K_STATE_FAILED; goto quick_exit; } + /* Undefined behavior, sometimes decode_tile_data doesn't + fill the buffer and we do things with it later, leading + to valgrind errors. */ + memset(new, 0, tile_info.data_size); state->buffer = new; buffer_size = tile_info.data_size; }