From 73f5102392c6efcdfdae58f4093a47443452ea13 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 24 Sep 2026 19:00:13 +0530 Subject: [PATCH] Graphics protocol: Fix memory leak when loading corrupt PNG with ICC profile --- docs/changelog.rst | 3 +++ kitty/png-reader.c | 10 ++++++---- kitty_tests/graphics.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f5dae0a0..baaff1006 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -205,6 +205,9 @@ Detailed list of changes - Fix an out-of-bounds memory access when drawing a multi-line text sized character with the cursor below the bottom margin of the scroll region +- Fix a memory leak when loading a corrupt PNG image that has an embedded ICC + color profile + 0.49.1 [2026-09-24] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/png-reader.c b/kitty/png-reader.c index 9d68b0fd7..c7536122a 100644 --- a/kitty/png-reader.c +++ b/kitty/png-reader.c @@ -64,6 +64,10 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz, int max_im png_structp png = NULL; png_infop info = NULL; struct custom_error_handler eh = {.d = d}; + // volatile because these are modified after setjmp() and read after a + // longjmp() in the error path, so they must not be cached in registers + cmsHPROFILE volatile input_profile = NULL; + cmsHTRANSFORM volatile colorspace_transform = NULL; png = png_create_read_struct(PNG_LIBPNG_VER_STRING, &eh, read_png_error_handler, read_png_warn_handler); if (!png) ABRT(ENOMEM, "Failed to create PNG read structure"); info = png_create_info_struct(png); @@ -82,8 +86,6 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz, int max_im bit_depth = png_get_bit_depth(png, info); double image_gamma; int intent; - cmsHPROFILE input_profile = NULL; - cmsHTRANSFORM colorspace_transform = NULL; if (png_get_sRGB(png, info, &intent)) { // do nothing since we output sRGB } else if (png_get_gAMA(png, info, &image_gamma)) { @@ -132,12 +134,12 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz, int max_im if (colorspace_transform) { for (int i = 0; i < d->height; i++) { cmsDoTransform(colorspace_transform, d->row_pointers[i], d->row_pointers[i], d->width); } - cmsDeleteTransform(colorspace_transform); } - if (input_profile) cmsCloseProfile(input_profile); d->ok = true; err: + if (colorspace_transform) cmsDeleteTransform(colorspace_transform); + if (input_profile) cmsCloseProfile(input_profile); if (png) png_destroy_read_struct(&png, info ? &info : NULL, NULL); return; } diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index e7fa1bfab..de41d8361 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -697,6 +697,17 @@ class TestGraphics(BaseTest): # while libpng still expects more compressed data. truncated = full[:-16] self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, truncated) + # Same, but with an embedded ICC profile so that lcms2 handles are + # allocated before libpng longjmps out, they must be freed in the + # error path (detected by LeakSanitizer in ASAN builds). + try: + from PIL import ImageCms + except ImportError: + return + icc = ImageCms.ImageCmsProfile(ImageCms.createProfile('sRGB')).tobytes() + buf = BytesIO() + Image.frombytes('RGBA', (w, h), byte_block(w * h * 4)).save(buf, 'PNG', icc_profile=icc) + self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, buf.getvalue()[:-16]) def test_gr_operations_with_numbers(self): s = self.create_screen()