Skip to content

Commit 256f6ea

Browse files
authored
Valgrind Memory Leak Checking (#8954)
2 parents f34b4a1 + 399b6c1 commit 256f6ea

File tree

12 files changed

+141
-28
lines changed

12 files changed

+141
-28
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Test Valgrind Memory Leaks
2+
3+
# like the Docker tests, but running valgrind only on *.c/*.h changes.
4+
5+
# this is very expensive. Only run on the pull request.
6+
on:
7+
# push:
8+
# branches:
9+
# - "**"
10+
# paths:
11+
# - ".github/workflows/test-valgrind.yml"
12+
# - "**.c"
13+
# - "**.h"
14+
pull_request:
15+
paths:
16+
- ".github/workflows/test-valgrind.yml"
17+
- "**.c"
18+
- "**.h"
19+
- "depends/docker-test-valgrind-memory.sh"
20+
workflow_dispatch:
21+
22+
permissions:
23+
contents: read
24+
25+
concurrency:
26+
group: ${{ github.workflow }}-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
jobs:
30+
build:
31+
32+
runs-on: ubuntu-latest
33+
strategy:
34+
fail-fast: false
35+
matrix:
36+
docker: [
37+
ubuntu-22.04-jammy-amd64-valgrind,
38+
]
39+
dockerTag: [main]
40+
41+
name: ${{ matrix.docker }}
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
with:
46+
persist-credentials: false
47+
48+
- name: Build system information
49+
run: python3 .github/workflows/system-info.py
50+
51+
- name: Docker pull
52+
run: |
53+
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
54+
55+
- name: Build and Run Valgrind
56+
run: |
57+
# The Pillow user in the docker container is UID 1001
58+
sudo chown -R 1001 $GITHUB_WORKSPACE
59+
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
60+
sudo chown -R runner $GITHUB_WORKSPACE

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,18 @@ test-p:
106106
.PHONY: valgrind
107107
valgrind:
108108
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
109-
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
109+
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
110110
--log-file=/tmp/valgrind-output \
111111
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
112112

113+
.PHONY: valgrind-leak
114+
valgrind-leak:
115+
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
116+
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \
117+
--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \
118+
--log-file=/tmp/valgrind-output \
119+
python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output
120+
113121
.PHONY: readme
114122
readme:
115123
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2

Tests/oss-fuzz/python.supp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,23 @@
1414
fun:_TIFFReadEncodedTileAndAllocBuffer
1515
...
1616
}
17+
18+
{
19+
<python_alloc_possible_leak>
20+
Memcheck:Leak
21+
match-leak-kinds: all
22+
fun:malloc
23+
fun:_PyMem_RawMalloc
24+
fun:PyObject_Malloc
25+
...
26+
}
27+
28+
{
29+
<python_realloc_possible_leak>
30+
Memcheck:Leak
31+
match-leak-kinds: all
32+
fun:malloc
33+
fun:_PyMem_RawRealloc
34+
fun:PyMem_Realloc
35+
...
36+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
## Run this as the test script in the Docker valgrind image.
4+
## Note -- can be included directly into the Docker image,
5+
## but requires the current python.supp.
6+
7+
source /vpy3/bin/activate
8+
cd /Pillow
9+
make clean
10+
make install
11+
make valgrind-leak

src/_imaging.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) {
22262226
}
22272227

22282228
if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) {
2229+
ImagingDelete(imOut);
22292230
return NULL;
22302231
}
22312232

src/_imagingft.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ text_layout_raqm(
275275
if (!text || !size) {
276276
/* return 0 and clean up, no glyphs==no size,
277277
and raqm fails with empty strings */
278+
PyMem_Free(text);
278279
goto failed;
279280
}
280281
set_text = raqm_set_text(rq, text, size);
@@ -425,6 +426,7 @@ text_layout_fallback(
425426
"setting text direction, language or font features is not supported "
426427
"without libraqm"
427428
);
429+
return 0;
428430
}
429431

430432
if (PyUnicode_Check(string)) {

src/_webp.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
641641
ImagingSectionLeave(&cookie);
642642

643643
WebPPictureFree(&pic);
644+
645+
output = writer.mem;
646+
ret_size = writer.size;
647+
644648
if (!ok) {
645649
int error_code = (&pic)->error_code;
646650
char message[50] = "";
@@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
652656
);
653657
}
654658
PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message);
659+
free(output);
655660
return NULL;
656661
}
657-
output = writer.mem;
658-
ret_size = writer.size;
659662

660663
{
661664
/* I want to truncate the *_size items that get passed into WebP

src/encode.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
703703
return NULL;
704704
}
705705

706+
encoder->cleanup = ImagingLibTiffEncodeCleanup;
707+
706708
num_core_tags = sizeof(core_tags) / sizeof(int);
707709
for (pos = 0; pos < tags_size; pos++) {
708710
item = PyList_GetItemRef(tags, pos);

src/libImaging/Arrow.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ ReleaseExportedSchema(struct ArrowSchema *array) {
3636
child->release(child);
3737
child->release = NULL;
3838
}
39-
// UNDONE -- should I be releasing the children?
39+
free(array->children[i]);
40+
}
41+
if (array->children) {
42+
free(array->children);
4043
}
4144

4245
// Release dictionary
@@ -117,6 +120,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
117120
retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel");
118121
if (retval != 0) {
119122
free(schema->children[0]);
123+
free(schema->children);
120124
schema->release(schema);
121125
return retval;
122126
}
@@ -127,9 +131,7 @@ static void
127131
release_const_array(struct ArrowArray *array) {
128132
Imaging im = (Imaging)array->private_data;
129133

130-
if (array->n_children == 0) {
131-
ImagingDelete(im);
132-
}
134+
ImagingDelete(im);
133135

134136
// Free the buffers and the buffers array
135137
if (array->buffers) {

src/libImaging/JpegEncode.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
131131
break;
132132
default:
133133
state->errcode = IMAGING_CODEC_CONFIG;
134+
jpeg_destroy_compress(&context->cinfo);
134135
return -1;
135136
}
136137

@@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
161162
/* Would subsample the green and blue
162163
channels, which doesn't make sense */
163164
state->errcode = IMAGING_CODEC_CONFIG;
165+
jpeg_destroy_compress(&context->cinfo);
164166
return -1;
165167
}
166168
jpeg_set_colorspace(&context->cinfo, JCS_RGB);

src/libImaging/TiffDecode.c

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) {
929929
return status;
930930
}
931931

932+
int
933+
ImagingLibTiffEncodeCleanup(ImagingCodecState state) {
934+
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
935+
TIFF *tiff = clientstate->tiff;
936+
937+
if (!tiff) {
938+
return 0;
939+
}
940+
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
941+
if (clientstate->fp) {
942+
// Python will manage the closing of the file rather than libtiff
943+
// So only call TIFFCleanup
944+
TIFFCleanup(tiff);
945+
} else {
946+
// When tif_closeproc refers to our custom _tiffCloseProc though,
947+
// that is fine, as it does not close the file
948+
TIFFClose(tiff);
949+
}
950+
return 0;
951+
}
952+
932953
int
933954
ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) {
934955
/* One shot encoder. Encode everything to the tiff in the clientstate.
@@ -1010,16 +1031,6 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
10101031
TRACE(("Encode Error, row %d\n", state->y));
10111032
state->errcode = IMAGING_CODEC_BROKEN;
10121033

1013-
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
1014-
if (clientstate->fp) {
1015-
// Python will manage the closing of the file rather than libtiff
1016-
// So only call TIFFCleanup
1017-
TIFFCleanup(tiff);
1018-
} else {
1019-
// When tif_closeproc refers to our custom _tiffCloseProc though,
1020-
// that is fine, as it does not close the file
1021-
TIFFClose(tiff);
1022-
}
10231034
if (!clientstate->fp) {
10241035
free(clientstate->data);
10251036
}
@@ -1036,22 +1047,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
10361047
TRACE(("Error flushing the tiff"));
10371048
// likely reason is memory.
10381049
state->errcode = IMAGING_CODEC_MEMORY;
1039-
if (clientstate->fp) {
1040-
TIFFCleanup(tiff);
1041-
} else {
1042-
TIFFClose(tiff);
1043-
}
10441050
if (!clientstate->fp) {
10451051
free(clientstate->data);
10461052
}
10471053
return -1;
10481054
}
1049-
TRACE(("Closing \n"));
1050-
if (clientstate->fp) {
1051-
TIFFCleanup(tiff);
1052-
} else {
1053-
TIFFClose(tiff);
1054-
}
10551055
// reset the clientstate metadata to use it to read out the buffer.
10561056
clientstate->loc = 0;
10571057
clientstate->size = clientstate->eof; // redundant?

src/libImaging/TiffDecode.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset);
4040
extern int
4141
ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp);
4242
extern int
43+
ImagingLibTiffEncodeCleanup(ImagingCodecState state);
44+
extern int
4345
ImagingLibTiffMergeFieldInfo(
4446
ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length
4547
);

0 commit comments

Comments
 (0)