Skip to content

Fix use-after-free when a CanvasPattern outlives its source Image#2583

Closed
iurisilvio wants to merge 1 commit into
Automattic:masterfrom
iurisilvio:fix/canvas-pattern-source-retention
Closed

Fix use-after-free when a CanvasPattern outlives its source Image#2583
iurisilvio wants to merge 1 commit into
Automattic:masterfrom
iurisilvio:fix/canvas-pattern-source-retention

Conversation

@iurisilvio

@iurisilvio iurisilvio commented May 18, 2026

Copy link
Copy Markdown
Contributor

Use-after-free: a CanvasPattern built from an Image can outlive the pixel buffer it points at

Reopening — this was closed as unnecessary, but the case below is a genuine use-after-free, not just defensive retention.

Root cause

Pattern::Pattern builds its cairo pattern from the source surface:

_pattern = cairo_pattern_create_for_surface(surface);   // src/CanvasPattern.cc

For images decoded from JPEG/GIF/raw data, that surface is created with cairo_image_surface_create_for_data(_data, …) — the pixel buffer _data is owned by the Image, not by cairo. When the source Image is GC'd, Image::clearData() runs:

void Image::clearData() {
  if (_surface) { cairo_surface_destroy(_surface); … }
  delete[] _data;     // unconditional — even if a pattern still references the surface
  _data = nullptr;
}

cairo_pattern_create_for_surface references the surface object (so cairo_surface_destroy doesn't free the struct), but nothing keeps _data alive and clearData() frees it unconditionally. Any later use of the pattern reads freed memory.

Repro (node --expose-gc repro.js)

const { createCanvas, loadImage } = require('canvas')
;(async () => {
  const src = createCanvas(128, 128); src.getContext('2d').fillRect(0, 0, 128, 128)
  const jpeg = src.toBuffer('image/jpeg')        // decodes via create_for_data

  const out = createCanvas(256, 256)
  const ctx = out.getContext('2d')
  let pattern
  {
    const img = await loadImage(jpeg)
    pattern = ctx.createPattern(img, 'repeat')   // pattern references img's surface
  }                                              // no JS ref to img survives
  for (let i = 0; i < 10; i++) global.gc()       // ~Image -> clearData() -> delete[] _data

  ctx.fillStyle = pattern
  ctx.fillRect(0, 0, 256, 256)                   // reads freed _data
  out.toBuffer('image/png')                      // UAF
})()

An ASan build reports heap-use-after-free in 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 for create_for_data surfaces (JPEG/GIF/raw), where the buffer lifetime is tied to the Image and freed in clearData(). 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/Canvas for 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 source Image; without it the repro above faults.

…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 iurisilvio closed this Jun 22, 2026
@iurisilvio iurisilvio reopened this Jun 22, 2026
@iurisilvio iurisilvio changed the title Retain source Image/Canvas in CanvasPattern so the cairo surface stays valid Fix use-after-free when a CanvasPattern outlives its source Image Jun 22, 2026
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>
@iurisilvio

Copy link
Copy Markdown
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.

@iurisilvio iurisilvio closed this Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant