Skip to content

Take ownership of cairo_pattern_t in canvas_state_t with refcounts#2582

Open
iurisilvio wants to merge 1 commit into
Automattic:masterfrom
iurisilvio:fix/canvas-state-pattern-refcount
Open

Take ownership of cairo_pattern_t in canvas_state_t with refcounts#2582
iurisilvio wants to merge 1 commit into
Automattic:masterfrom
iurisilvio:fix/canvas-state-pattern-refcount

Conversation

@iurisilvio
Copy link
Copy Markdown

Fixes #2578.

canvas_state_t holds four cairo_pattern_t* members (fillPattern, strokePattern, fillGradient, strokeGradient) but the destructor only freed fontDescription. Every Context2d that ended life with any non-null pattern leaked it. The copy constructor copied the raw pointers — save()/restore() chains shared the same cairo_pattern_t* across states with no shared ownership.

This PR takes proper refcounted ownership:

  • canvas_state_t copy ctor + new operator= cairo_pattern_reference each pattern.
  • ~canvas_state_t cairo_pattern_destroys all four.
  • New helpers setFillPattern / setStrokePattern / setFillGradient / setStrokeGradient / clearFillPattern / clearStrokePattern handle replacement at the call sites (SetFillStyle / SetStrokeStyle / _setFillColor / _setStrokeColor).

Related correctness fixes that become observable when finalizers run promptly (the NAPI_EXPERIMENTAL finalizer path):

  • _fillStyle and _strokeStyle use Reset(obj, 1) (strong ref) so the JS CanvasGradient / CanvasPattern object stays alive while the Context2d state still references its native cairo_pattern_t.
  • ~Context2d pops all states before destroying _layout/_context so ~canvas_state_t runs with a valid context. The destructor intentionally does not call _resetPersistentHandles() itself — napi_delete_reference is unsafe inside GC. Handles are reset only from Finalize(env), which runs deferred via node_api_post_finalizer.
  • resetState uses a while (!states.empty()) states.pop() loop and re-points state = &states.top() after emplace(); the original pop()+emplace() left state dangling when states was empty.

Standalone repro for the leak portion is in #2578 — ~5 KiB/iter sustained on a gradient loop, slope drops to allocator noise after this PR.

canvas_state_t held four cairo_pattern_t* members (fillPattern,
strokePattern, fillGradient, strokeGradient) but the destructor only
freed fontDescription. Every Context2d that ended life with a non-null
pattern leaked the pattern. The copy constructor copied the raw
pointers, so save()/restore() shared the same pattern across states
without refcounting — making any per-state destroy unsafe.

Switch to proper cairo refcount discipline:
- canvas_state_t copy ctor + new operator= reference each pattern.
- ~canvas_state_t releases all four patterns.
- New setFillPattern / setStrokePattern / setFillGradient /
  setStrokeGradient helpers replace the previous pattern (refcount--)
  before storing the new one (refcount++). clearFillPattern /
  clearStrokePattern release both pattern and gradient when switching
  to a solid color.

Also fix correctness issues that become visible once finalizers run
promptly (NAPI_EXPERIMENTAL path):
- _fillStyle and _strokeStyle now use Reset(obj, 1) (strong ref) so
  the JS gradient/pattern object stays alive while the Context2d's
  state still references its native cairo_pattern_t.
- ~Context2d pops all states before destroying _layout and _context
  so canvas_state_t destructors run with a valid context. The
  destructor intentionally does NOT call _resetPersistentHandles()
  itself — that invokes napi_delete_reference which is unsafe inside
  GC. The handles are reset from Finalize(env), which runs deferred
  via node_api_post_finalizer.
- resetState clears all states (not just pop()) and re-points 'state'
  at the freshly emplaced top; the original pop()+emplace() left
  'state' dangling when states was empty.

Fixes Automattic#2578
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.

canvas_state_t leaks cairo_pattern_t members; fillStyle/strokeStyle are weak refs

1 participant