Skip to content

canvas_state_t leaks cairo_pattern_t members; fillStyle/strokeStyle are weak refs #2578

@iurisilvio

Description

@iurisilvio

(This issue makes sense only after #2562)

Bug

canvas_state_t (CanvasRenderingContext2d.h:19-72) holds four cairo_pattern_t* members (fillPattern, strokePattern, fillGradient, strokeGradient) but the destructor only frees fontDescription. Every time a state is destroyed with any of those four pointers non-null, the cairo pattern leaks.
Additionally, the copy constructor copies the raw pointers without cairo_pattern_reference(). Multiple states (from save()/restore() chains) end up sharing the same cairo_pattern_t* with no shared ownership — so freeing in any state's destructor would dangle the others.
A related correctness issue: Context2d::_fillStyle / Context2d::_strokeStyle are weak references (.Reset(obj), refcount 0). Under prompt finalization (e.g. when NAPI_EXPERIMENTAL is enabled and node_api_post_finalizer runs the finalizer at a safe point), V8 can garbage-collect the JS CanvasGradient / CanvasPattern object while the Context2d's state still references its native cairo_pattern_t. Subsequent reads of ctx.fillStyle return a stale handle.

Reproduction

'use strict';
if (typeof gc !== 'function') {
  console.error('Run with --expose-gc');
  process.exit(1);
}
const canvas = require('canvas');
const ITER = parseInt(process.argv[2] || '20000', 10);
const MODE = process.argv[3] || 'gradient';   // 'gradient' or 'color' (control)
const mb = (n) => +(n / 1024 / 1024).toFixed(1);
(async () => {
  gc(); gc();
  const baseline = mb(process.memoryUsage().rss);
  let last = baseline;
  console.log(`mode=${MODE} iter=${ITER} baseline rss=${baseline} MiB`);
  for (let i = 1; i <= ITER; i++) {
    const c = canvas.createCanvas(200, 200);
    const ctx = c.getContext('2d');
    if (MODE === 'gradient') {
      const grad = ctx.createLinearGradient(0, 0, 200, 200);
      grad.addColorStop(0, 'red');
      grad.addColorStop(1, 'blue');
      ctx.fillStyle = grad;
    } else {
      ctx.fillStyle = 'red';
    }
    ctx.fillRect(0, 0, 200, 200);
    c.width = 0; c.height = 0;
    if (i % 2000 === 0) {
      gc(); gc();
      const rss = mb(process.memoryUsage().rss);
      console.log(`iter=${i} rss=${rss}M slope=${(((rss - last) * 1024) / 2000).toFixed(2)} KiB/iter`);
      last = rss;
    }
  }
})();

Observed on canvas@3.2.3 from npm (Linux arm64, Node 20)

mode=gradient iter=10000 baseline rss=46 MiB
iter=2000  rss=51.7M  slope=2.92 KiB/iter
iter=4000  rss=60.5M  slope=4.51 KiB/iter
iter=6000  rss=70.4M  slope=5.07 KiB/iter
iter=8000  rss=80.0M  slope=4.92 KiB/iter
iter=10000 rss=89.6M  slope=4.92 KiB/iter
per-iter=4.97 KiB

Slope is consistent ~5 KiB/iter — each iteration leaks roughly one cairo_pattern_t worth (a few hundred bytes for solid gradients plus cairo's internal metadata). Slope on mode=color is ~0 since the four pattern fields stay null.

Fix

Take refcounted ownership in canvas_state_t:

  1. Setters refcount the new pattern (cairo_pattern_reference) and release the previous one (cairo_pattern_destroy).
  2. Copy constructor and operator= reference each pattern instead of copying raw pointers.
  3. Destructor releases all four patterns.
  4. New helpers setFillPattern / setStrokePattern / setFillGradient / setStrokeGradient / clearFillPattern / clearStrokePattern enforce the discipline at the call sites in Context2d::SetFillStyle / Context2d::SetStrokeStyle / _setFillColor / _setStrokeColor.
    For the style retention bug, change _fillStyle.Reset(obj) and _strokeStyle.Reset(obj) to Reset(obj, 1) so the JS gradient/pattern object stays alive as long as the Context2d references its native cairo_pattern_t.
    The same change should also clear the state stack in ~Context2d before destroying _layout/_context so state destructors run with a valid context, and replace resetState's single states.pop() with a while (!states.empty()) states.pop() followed by states.emplace() + state = &states.top() (the original pop() underflows if states is empty and leaves state dangling).

Affects

Any application that uses gradients or patterns in a canvas pipeline (data viz, charting, complex image processing). The bug is in core Context2d state ownership.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions