Skip to content

Release fix memory leak - #1

Open
iurisilvio wants to merge 20 commits into
masterfrom
release-fix-memory-leak
Open

Release fix memory leak#1
iurisilvio wants to merge 20 commits into
masterfrom
release-fix-memory-leak

Conversation

@iurisilvio

Copy link
Copy Markdown
Owner

Thanks for contributing!

  • Have you updated CHANGELOG.md?

iurisilvio and others added 10 commits May 17, 2026 14:06
The rotate90 and rotate270 lambdas in rotatePixels() allocated a
temporary 'unrotated' buffer via 'new uint8_t[n_bytes]' but never
freed it. Every JPEG decoded with EXIF orientation 90, 180, or 270
leaked the full pixel buffer (width * height * 4 bytes) — typically
several MiB per call.

Also corrects a 'new[]' / 'free()' allocator mismatch in
decodeJPEGIntoSurface's OOM error path (delete[] is the correct
deallocator for arrays allocated with new[]).
loadImage's onerror handler rejects the Promise but leaves image.src
pointing at the input buffer. When libjpeg/libpng allocated a cairo
surface before failing mid-decode, that surface stays attached to the
Image until V8 GC — under sustained load on malformed inputs this
delays cleanup arbitrarily.

Assign Buffer.alloc(0) before reject so clearData() runs synchronously.
If libjpeg calls error_exit during jpeg_read_scanlines (corrupt or
truncated JPEG that decoded the header but fails in the scan), it
longjmps back to the setjmp point in loadJPEGFromBuffer. That bypasses
the cleanup at the bottom of decodeJPEGIntoSurface, leaking both
'data' (width * height * 4) and 'src' (width * output_components).

Store both pointers on the canvas_jpeg_error_mgr so the error handler
can delete[] them before longjmp. Also reorder cleanup to destroy
libjpeg state and free src before creating the cairo surface, so the
success path is symmetric with the new failure path.
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 two correctness issues exposed by prompt finalization
(NAPI_EXPERIMENTAL path from PR Automattic#2562):
- _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 valid context.
- resetState clears all states (not just pop()) and re-points 'state'
  at the freshly emplaced top.
Bundles four additional fixes on top of v3.2.3-memfix.1:

- index.js loadImage clears image.src on error (defensive cleanup of
  partial cairo surfaces from malformed inputs).
- decodeJPEGIntoSurface frees data/src when libjpeg longjmps from
  jpeg_read_scanlines mid-decode (corrupt scan data).
- canvas_state_t takes refcounted ownership of cairo_pattern_t members
  (fillPattern, strokePattern, fillGradient, strokeGradient) so they
  don't leak when state is destroyed.
- _fillStyle/_strokeStyle hold strong refs to gradient/pattern JS
  objects so they can't be GC'd while in use under prompt finalization.
…unts

Don't call _resetPersistentHandles() from ~Context2d. napi_delete_reference
is unsafe inside GC and Node 22 panics with:

  FATAL ERROR: Finalizer is calling a function that may affect GC state.
  The finalizers are run directly from GC and must not affect GC state.

Persistent handles are already reset from Finalize(Napi::Env), which runs
deferred via node_api_post_finalizer when NAPI_EXPERIMENTAL is enabled.
…emfix.3)

The nogc / node_api_nogc_finalize approach (Automattic#2562, Automattic#2436) relied on
experimental N-API and did not fix the leak in practice. Revert to
node-addon-api ^7.0.0 and drop NAPI_EXPERIMENTAL.

- Remove the experimental-only Pattern::Finalize override; _source is
  released by its Napi::Reference destructor under deferred finalization.
- Restore _resetPersistentHandles() in ~Context2d (safe again now that the
  destructor no longer runs inside GC).
- Keep Automattic#2582's cairo_pattern_t refcounting and the CanvasPattern _source /
  strong fillStyle/strokeStyle refs: these guard a real use-after-free of
  Image create_for_data surfaces (Image::clearData frees the pixel buffer
  unconditionally), independent of finalizer timing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test/memory.test.js asserted synchronous freeing (the nogc behavior) via a
synchronous gc() loop, so it fails under standard deferred N-API finalization
(RSS ~468 MiB). Drain finalizers via setImmediate before measuring and assert
RSS < 256 MiB (settles ~110 MiB when freed vs ~400+ MiB if leaked), so it
still catches a real cairo-surface leak.

Also fix the `test` script: mocha 5.2.0 ignores `--node-option expose-gc`, so
gc was never exposed and this test was silently skipped (pending). Invoke
mocha via `node --expose-gc` so it actually runs in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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