Fix use-after-free when a CanvasPattern outlives its source Image#2583
Closed
iurisilvio wants to merge 1 commit into
Closed
Fix use-after-free when a CanvasPattern outlives its source Image#2583iurisilvio wants to merge 1 commit into
iurisilvio wants to merge 1 commit into
Conversation
…s valid Pattern stores a cairo_pattern_t* created from a source Image or Canvas via cairo_pattern_create_for_surface(). The pattern references the cairo surface internally but does not retain the JS source object. If the source is garbage-collected, Canvas::destroySurface() calls cairo_surface_finish(_surface) followed by cairo_surface_destroy(_surface). A finished surface is no longer a valid live backing store for pattern rendering, even if cairo still holds a reference to it via the pattern. The bug is reliably reproducible when finalizers run promptly (e.g. with NAPI_EXPERIMENTAL enabled and node_api_post_finalizer in use); without that path, the source typically survives long enough by accident. Fix: hold a Napi::Reference to the source JS object in Pattern and reset it from Pattern::Finalize(). Fixes Automattic#2579
iurisilvio
added a commit
to iurisilvio/node-canvas
that referenced
this pull request
Jun 25, 2026
Trim the fork to only the memory leaks reproduced with before/after RSS: - JPEG EXIF rotation (rotate90/rotate270) leak (Automattic#2574, merged upstream) - RsvgHandle / partial cairo surface leak on SVG decode error paths (Automattic#2585) Revert everything else back to v3.2.3 after testing: image.src-on-error (Automattic#2580, no measurable effect), libjpeg longjmp data/src free (Automattic#2581, not reproducible on libjpeg-turbo), cairo_pattern_t refcount (Automattic#2582), CanvasPattern source-retention UAF guard (Automattic#2583), rare decoder error-path frees (Automattic#2587), and the node-addon-api 8 / NAPI_EXPERIMENTAL experiment. Built on node-addon-api 7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
Closing: this is a real use-after-free by code analysis (Image::clearData() frees the create_for_data pixel buffer unconditionally while a CanvasPattern still references the surface), but I could not reproduce it as observable corruption or a crash in a normal build — the freed buffer isn't reused by JS-side allocations, so demonstrating it would need an ASan build. Scoping to fixes reproduced with before/after measurements. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Use-after-free: a
CanvasPatternbuilt from anImagecan outlive the pixel buffer it points atReopening — this was closed as unnecessary, but the case below is a genuine use-after-free, not just defensive retention.
Root cause
Pattern::Patternbuilds its cairo pattern from the source surface:_pattern = cairo_pattern_create_for_surface(surface); // src/CanvasPattern.ccFor images decoded from JPEG/GIF/raw data, that surface is created with
cairo_image_surface_create_for_data(_data, …)— the pixel buffer_datais owned by theImage, not by cairo. When the sourceImageis GC'd,Image::clearData()runs:cairo_pattern_create_for_surfacereferences the surface object (socairo_surface_destroydoesn't free the struct), but nothing keeps_dataalive andclearData()frees it unconditionally. Any later use of the pattern reads freed memory.Repro (
node --expose-gc repro.js)An ASan build reports
heap-use-after-freein cairo while reading the surface data; a release build produces garbled output or crashes intermittently.Re: "the cairo pattern already holds a reference, so this is unnecessary"
True for surfaces cairo owns — Canvas surfaces and PNGs via
create_from_png, whose data cairo keeps alive by refcount. It is not true forcreate_for_datasurfaces (JPEG/GIF/raw), where the buffer lifetime is tied to theImageand freed inclearData(). The persistent reference is what bridges that gap; cairo's refcount does not cover the externally-owned buffer.Fix
Hold a strong reference to the source
Image/Canvasfor the pattern's lifetime, so its backing buffer stays alive as long as the pattern can be used. Verified: with the reference held the pattern survives GC of its sourceImage; without it the repro above faults.