Skip to content

Commit c701781

Browse files
sideshowbarkergmta
authored andcommitted
LibGfx: Fix PNG decoder leaks from libpng error longjmp
Problem: Runs of the Linux Sanitizers intermittently fail with LeakSanitizer leaks of ~45 KB across 21 allocations from PNGLoadingContext::read_frames() — triggered when the test suite decodes malformed PNGs. Cause: When libpng hits a corrupted IDAT chunk inside png_read_image(), it longjmps back to the setjmp landing pad in PNGLoadingContext::read_all_frames(). longjmp unwinds the stack without running C++ destructors — so the stack-locals in read_frames (Vector<u8*> row_pointers, the in-flight Bitmap inside decode_frame, and the APNG branch’s output buffer and Painter) leak their heap storage. Fix: Promote those stack-locals to members of PNGLoadingContext (which is heap-allocated and outlives the setjmp scope) — so their storage is reachable by RAII when the context is destroyed. Clear them in the setjmp error handler too — so memory is released promptly on the error path, rather than waiting until ~PNGLoadingContext().
1 parent 1050981 commit c701781

3 files changed

Lines changed: 43 additions & 6 deletions

File tree

Libraries/LibGfx/ImageFormats/PNGLoader.cpp

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,32 @@ struct PNGLoadingContext {
3434
Optional<ByteBuffer> icc_profile;
3535
OwnPtr<ExifMetadata> exif_metadata;
3636

37+
// Working state for read_frames(). These live as members (rather than as stack-locals inside read_frames()) — so
38+
// that a libpng longjmp out of the setjmp scope below doesn't bypass C++ destructors, and leak the heap storage
39+
// they own. The setjmp handler clears them, so the memory gets released promptly on the error path.
40+
Vector<u8*> row_pointers;
41+
RefPtr<Bitmap> in_flight_bitmap;
42+
RefPtr<Bitmap> output_buffer;
43+
OwnPtr<Painter> painter;
44+
45+
void clear_read_frames_working_state()
46+
{
47+
row_pointers.clear();
48+
in_flight_bitmap = nullptr;
49+
output_buffer = nullptr;
50+
painter = nullptr;
51+
}
52+
3753
ErrorOr<size_t> read_frames(png_structp, png_infop);
3854
ErrorOr<void> apply_exif_orientation();
3955

4056
ErrorOr<void> read_all_frames()
4157
{
4258
// NOTE: We need to setjmp() here because libpng uses longjmp() for error handling.
4359
if (auto error_value = setjmp(png_jmpbuf(png_ptr)); error_value) {
60+
// longjmp() bypassed the C++ destructors for any stack-locals in read_frames(); release the working-state
61+
// members explicitly here — so their heap storage doesn't sit around until ~PNGLoadingContext().
62+
clear_read_frames_working_state();
4463
return Error::from_errno(error_value);
4564
}
4665

@@ -255,25 +274,27 @@ ErrorOr<void> PNGLoadingContext::apply_exif_orientation()
255274

256275
ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop info_ptr)
257276
{
258-
Vector<u8*> row_pointers;
259277
auto decode_frame = [&](IntSize frame_size) -> ErrorOr<NonnullRefPtr<Bitmap>> {
260-
auto frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, frame_size));
278+
// Allocate into the in_flight_bitmap member — so the bitmap survives a potential libpng longjmp out of
279+
// png_read_image() below.
280+
in_flight_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, frame_size));
261281

262282
row_pointers.resize_and_keep_capacity(frame_size.height());
263283
for (auto i = 0; i < frame_size.height(); ++i)
264-
row_pointers[i] = frame_bitmap->scanline_u8(i);
284+
row_pointers[i] = in_flight_bitmap->scanline_u8(i);
265285

266286
png_read_image(png_ptr, row_pointers.data());
267-
return frame_bitmap;
287+
// Past the longjmp window; hand the bitmap to the caller, and clear the member.
288+
return in_flight_bitmap.release_nonnull();
268289
};
269290

270291
if (png_get_acTL(png_ptr, info_ptr, &frame_count, &loop_count)) {
271292
// acTL chunk present: This is an APNG.
272293
png_set_acTL(png_ptr, info_ptr, frame_count, loop_count);
273294

274295
// Conceptually, at the beginning of each play the output buffer must be completely initialized to a fully transparent black rectangle, with width and height dimensions from the `IHDR` chunk.
275-
auto output_buffer = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, size));
276-
auto painter = Painter::create(output_buffer);
296+
output_buffer = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, size));
297+
painter = Painter::create(*output_buffer);
277298
size_t animation_frame_count = 0;
278299

279300
for (size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
@@ -364,6 +385,9 @@ ErrorOr<size_t> PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in
364385
auto decoded_frame_bitmap = TRY(decode_frame(size));
365386
frame_descriptors.append({ move(decoded_frame_bitmap), 0 });
366387
}
388+
389+
// Clear the working state — so it doesn't outlive read_frames() on the success path.
390+
clear_read_frames_working_state();
367391
return frame_count;
368392
}
369393

Tests/LibGfx/TestImageDecoder.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,19 @@ TEST_CASE(test_png_malformed_frame)
516516
}
517517
}
518518

519+
// Regression test: libpng longjmp() out of png_read_image() must not leak the in-flight row-pointers buffer or bitmap
520+
// from PNGLoadingContext::read_frames(). The fixture has a valid IHDR, but a corrupted IDAT body that decompresses past
521+
// its zlib end-of-block marker — forcing libpng to longjmp mid-decode.
522+
TEST_CASE(test_png_corrupt_idat_does_not_leak_on_libpng_longjmp)
523+
{
524+
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("png/corrupt-idat.png"sv)));
525+
auto plugin_or_error = Gfx::PNGImageDecoderPlugin::create(file->bytes());
526+
// PNGImageDecoderPlugin::create() catches the libpng error and falls back to a single-frame placeholder — so we
527+
// don't assert on the result here. The test passes if the run completes without LeakSanitizer reports.
528+
if (!plugin_or_error.is_error())
529+
(void)plugin_or_error.release_value()->frame(0);
530+
}
531+
519532
TEST_CASE(test_png_large_dimensions)
520533
{
521534
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("png/65535x1.png"sv)));
8.8 KB
Loading

0 commit comments

Comments
 (0)