Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
74ab5ac
Fix memory leak in arrow export using array structure
wiredfool May 11, 2025
4984c45
valgrind memory leak check
wiredfool May 13, 2025
fdfba98
fix memory leak in arrow schema
wiredfool May 13, 2025
84b88a9
Suppress all python level leaks for now
wiredfool May 13, 2025
eaab435
Fix leak in webp_encode
wiredfool May 13, 2025
a9bcd7d
Fix leak of destination image in ImagingUnsharpMask when an error occurs
wiredfool May 13, 2025
e2e40c5
Fix memory leak in TiffEncode
wiredfool May 13, 2025
f792e0b
Fix memory leak
wiredfool May 13, 2025
789631c
Fix memory leak when JpegEncode returns an error.
wiredfool May 13, 2025
7aa6a61
Wrap Makefile
wiredfool May 13, 2025
fb126af
Adding pytest-valgrind install
wiredfool May 15, 2025
d5449d5
Guess so.
wiredfool May 15, 2025
218f055
Add github workflow/test-script
wiredfool May 15, 2025
a6b8b3a
executable
wiredfool May 15, 2025
2d506f6
correct target
wiredfool May 15, 2025
f1957b4
Xfail timouts in Valgrind tests
wiredfool May 16, 2025
6391f2c
Merge remote-tracking branch 'upstream/main' into valgrind-leakcheck
wiredfool May 16, 2025
ff50e30
Fix memory leak in text_layout_raqm on 0 length string
wiredfool May 16, 2025
20b49a3
Remove timeout as the specific reason,
wiredfool May 17, 2025
c35082b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 17, 2025
2603a24
Update depends/docker-test-valgrind-memory.sh
wiredfool May 23, 2025
60a1a20
add timeouts to two more tests
wiredfool May 23, 2025
c63db77
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2025
6096f33
Merge branch 'main' into valgrind-leakcheck
radarhere May 24, 2025
5b854b2
Merge branch 'main' into valgrind-leakcheck
radarhere May 27, 2025
98cf15e
Update depends/docker-test-valgrind-memory.sh
wiredfool May 30, 2025
399b6c1
Update Makefile
wiredfool May 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/test-valgrind-memory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Test Valgrind Memory Leaks

# like the Docker tests, but running valgrind only on *.c/*.h changes.

# this is very expensive. Only run on the pull request.
on:
# push:
# branches:
# - "**"
# paths:
# - ".github/workflows/test-valgrind.yml"
# - "**.c"
# - "**.h"
pull_request:
paths:
- ".github/workflows/test-valgrind.yml"
- "**.c"
- "**.h"
- "depends/docker-test-valgrind-memory.sh"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
docker: [
ubuntu-22.04-jammy-amd64-valgrind,
]
dockerTag: [main]

name: ${{ matrix.docker }}

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Build system information
run: python3 .github/workflows/system-info.py

- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}

- name: Build and Run Valgrind
run: |
# The Pillow user in the docker container is UID 1001
sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh
sudo chown -R runner $GITHUB_WORKSPACE
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,18 @@ test:
.PHONY: valgrind
valgrind:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
PILLOW_VALGRIND_TEST=true 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: valgrind-leak
valgrind-leak:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \
--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \
--log-file=/tmp/valgrind-output \
python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output

.PHONY: readme
readme:
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2
Expand Down
20 changes: 20 additions & 0 deletions Tests/oss-fuzz/python.supp
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,23 @@
fun:_TIFFReadEncodedTileAndAllocBuffer
...
}

{
<python_alloc_possible_leak>
Memcheck:Leak
match-leak-kinds: all
fun:malloc
fun:_PyMem_RawMalloc
fun:PyObject_Malloc
...
}

{
<python_realloc_possible_leak>
Memcheck:Leak
match-leak-kinds: all
fun:malloc
fun:_PyMem_RawRealloc
fun:PyMem_Realloc
...
}
11 changes: 11 additions & 0 deletions depends/docker-test-valgrind-memory.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

## Run this as the test script in the Docker valgrind image.
## Note -- can be included directly into the Docker image,
## but requires the current python.supp.

source /vpy3/bin/activate
cd /Pillow
make clean
make install
make valgrind-leak
1 change: 1 addition & 0 deletions src/_imaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -2226,6 +2226,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) {
}

if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) {
ImagingDelete(imOut);
return NULL;
}

Expand Down
2 changes: 2 additions & 0 deletions src/_imagingft.c
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ text_layout_raqm(
if (!text || !size) {
/* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */
PyMem_Free(text);
goto failed;
}
set_text = raqm_set_text(rq, text, size);
Expand Down Expand Up @@ -425,6 +426,7 @@ text_layout_fallback(
"setting text direction, language or font features is not supported "
"without libraqm"
);
return 0;
}

if (PyUnicode_Check(string)) {
Expand Down
7 changes: 5 additions & 2 deletions src/_webp.c
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
ImagingSectionLeave(&cookie);

WebPPictureFree(&pic);

output = writer.mem;
ret_size = writer.size;

if (!ok) {
int error_code = (&pic)->error_code;
char message[50] = "";
Expand All @@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
);
}
PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message);
free(output);
return NULL;
}
output = writer.mem;
ret_size = writer.size;

{
/* I want to truncate the *_size items that get passed into WebP
Expand Down
2 changes: 2 additions & 0 deletions src/encode.c
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
return NULL;
}

encoder->cleanup = ImagingLibTiffEncodeCleanup;

num_core_tags = sizeof(core_tags) / sizeof(int);
for (pos = 0; pos < tags_size; pos++) {
item = PyList_GetItemRef(tags, pos);
Expand Down
10 changes: 6 additions & 4 deletions src/libImaging/Arrow.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
child->release(child);
child->release = NULL;
}
// UNDONE -- should I be releasing the children?
free(array->children[i]);
}
if (array->children) {
free(array->children);
}

// Release dictionary
Expand Down Expand Up @@ -117,6 +120,7 @@
retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel");
if (retval != 0) {
free(schema->children[0]);
free(schema->children);

Check warning on line 123 in src/libImaging/Arrow.c

View check run for this annotation

Codecov / codecov/patch

src/libImaging/Arrow.c#L123

Added line #L123 was not covered by tests
schema->release(schema);
return retval;
}
Expand All @@ -127,9 +131,7 @@
release_const_array(struct ArrowArray *array) {
Imaging im = (Imaging)array->private_data;

if (array->n_children == 0) {
ImagingDelete(im);
}
ImagingDelete(im);

// Free the buffers and the buffers array
if (array->buffers) {
Expand Down
2 changes: 2 additions & 0 deletions src/libImaging/JpegEncode.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
break;
default:
state->errcode = IMAGING_CODEC_CONFIG;
jpeg_destroy_compress(&context->cinfo);

Check warning on line 134 in src/libImaging/JpegEncode.c

View check run for this annotation

Codecov / codecov/patch

src/libImaging/JpegEncode.c#L134

Added line #L134 was not covered by tests
return -1;
}

Expand Down Expand Up @@ -161,6 +162,7 @@
/* Would subsample the green and blue
channels, which doesn't make sense */
state->errcode = IMAGING_CODEC_CONFIG;
jpeg_destroy_compress(&context->cinfo);
return -1;
}
jpeg_set_colorspace(&context->cinfo, JCS_RGB);
Expand Down
42 changes: 21 additions & 21 deletions src/libImaging/TiffDecode.c
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,27 @@
return status;
}

int
ImagingLibTiffEncodeCleanup(ImagingCodecState state) {
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
TIFF *tiff = clientstate->tiff;

if (!tiff) {
return 0;

Check warning on line 938 in src/libImaging/TiffDecode.c

View check run for this annotation

Codecov / codecov/patch

src/libImaging/TiffDecode.c#L938

Added line #L938 was not covered by tests
}
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
if (clientstate->fp) {
// Python will manage the closing of the file rather than libtiff
// So only call TIFFCleanup
TIFFCleanup(tiff);
} else {
// When tif_closeproc refers to our custom _tiffCloseProc though,
// that is fine, as it does not close the file
TIFFClose(tiff);
}
return 0;
}

int
ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) {
/* One shot encoder. Encode everything to the tiff in the clientstate.
Expand Down Expand Up @@ -1010,16 +1031,6 @@
TRACE(("Encode Error, row %d\n", state->y));
state->errcode = IMAGING_CODEC_BROKEN;

// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
if (clientstate->fp) {
// Python will manage the closing of the file rather than libtiff
// So only call TIFFCleanup
TIFFCleanup(tiff);
} else {
// When tif_closeproc refers to our custom _tiffCloseProc though,
// that is fine, as it does not close the file
TIFFClose(tiff);
}
if (!clientstate->fp) {
free(clientstate->data);
}
Expand All @@ -1036,22 +1047,11 @@
TRACE(("Error flushing the tiff"));
// likely reason is memory.
state->errcode = IMAGING_CODEC_MEMORY;
if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
if (!clientstate->fp) {
free(clientstate->data);
}
return -1;
}
TRACE(("Closing \n"));
if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
// reset the clientstate metadata to use it to read out the buffer.
clientstate->loc = 0;
clientstate->size = clientstate->eof; // redundant?
Expand Down
2 changes: 2 additions & 0 deletions src/libImaging/TiffDecode.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset);
extern int
ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp);
extern int
ImagingLibTiffEncodeCleanup(ImagingCodecState state);
extern int
ImagingLibTiffMergeFieldInfo(
ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length
);
Expand Down
Loading