Skip to content

Handle a null surface from the image decoder callback - #1

Open
iurisilvio wants to merge 3 commits into
chearon:masterfrom
iurisilvio:fix-null-surface-from-decoder
Open

Handle a null surface from the image decoder callback#1
iurisilvio wants to merge 3 commits into
chearon:masterfrom
iurisilvio:fix-null-surface-from-decoder

Conversation

@iurisilvio

@iurisilvio iurisilvio commented Aug 20, 2026

Copy link
Copy Markdown

Loading an SVG whose <image> is in a format the embedder cannot decode segfaults.

loadImageResource() passes whatever createSurfaceForImage() returns straight to cairo_surface_set_user_data(). The callback has no way to signal "I cannot decode this" other than returning null — and for an arbitrary document that is routine, not exceptional — so the null goes to cairo and gets dereferenced. The same happens when no decoder is registered at all, since GraphicsCallbacks::createSurfaceForImage() returns null in that case too.

Where it came from

3fd8ba9 ("node-canvas patch"), the cairo port. Upstream has never had this shape: it returns the plutovg surface straight from loadImageResource(), and Bitmap is null-safe by construction — plutovg_surface_t* m_surface{nullptr} plus the isNull() that SVGImageElement::render() already checks.

-        return plutovg_surface_load_from_image_base64(input.data(), input.length());
+        plutovg_surface_load_from_image_base64(input.data(), input.length(), &data, &length);
+        if (data != NULL) {
+          cairo_surface_t* surface = callbacks.createSurfaceForImage(data, length);
+          cairo_surface_set_user_data(surface, nullptr, (void*)data, free);
+          return surface;
+        }

Worth saying because "just sync with upstream" would not help: this fork branched at 2872aff and upstream has exactly one commit since, an unrelated two-line parser change. The null-safety upstream gets for free lives in the plutovg path the port replaced.

Changes

  • Return a null Bitmap when the callback yields no surface, freeing the decoded data the surface never took ownership of. SVGImageElement::render() already skips a null image.
  • Do the same for a surface that comes back in an error state: cairo_surface_set_user_data() refuses to store on an error surface, so data would leak there as well.
  • Add test/decoderfn.cpp, covering all three things the callback can hand back — nothing, an error surface, and a usable surface. The last case also checks the image is still drawn and that the decoded bytes stay attached to the surface, so the fix cannot quietly turn every embedded image into a no-op. Runs under ctest.
  • Find cairo in the cmake build, without which none of this compiles standalone.

That last one is the odd commit out, so: plutovg.h and lunasvg.h have both included <cairo.h> since the port to cairo, but the build never looked for it — node-canvas's zig build supplies the include path, so cmake . here fails on the first file that pulls either header in. I left the meson build alone; its plutovg wrap still points at upstream sammycage/plutovg, which predates the cairo port, so cairo is not what stands between it and building this fork. I also did not touch .github/workflows/main.yml, since making its three OSes carry cairo is a bigger decision than this PR should make.

Verification

macOS arm64, cairo 1.18.4 from homebrew:

$ cmake -S . -B build && cmake --build build && (cd build && ctest --output-on-failure)
1/1 Test #1: decoderfn ........................   Passed

With source/svgelement.cpp reverted to master, the same test binary:

1/1 Test #1: decoderfn ........................***Exception: SegFault

Reproduced first through node-canvas, which uses this fork and registers a decoder that returns null for anything it cannot sniff as PNG, JPEG, GIF or BMP — Automattic/node-canvas#2613 carries the same case as a JS-level test, and its full suite (308 tests) passes against this branch on Linux, macOS and Windows.

iurisilvio and others added 3 commits August 20, 2026 13:22
plutovg.h and lunasvg.h both include <cairo.h> since the port to cairo, but
the build never looks for it, so a standalone `cmake .` fails on the first
source file that pulls either header in. The zig build in node-canvas passes
the include path itself, which is why this went unnoticed.

Look cairo up with pkg-config and carry it on the targets: privately for
plutovg's own sources, publicly for lunasvg, whose header hands out
cairo_surface_t.

The meson build is left alone. Its plutovg wrap still points at upstream
sammycage/plutovg, which predates the cairo port, so cairo is not what stands
between it and building this fork.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
loadImageResource() passed whatever createSurfaceForImage() returned straight
to cairo_surface_set_user_data(). The callback has no way to report a format
it cannot decode other than returning null, which is routine for an arbitrary
document, so a `<image>` in an unsupported or corrupt format segfaulted the
caller.

Return a null Bitmap in that case, and in the case of a surface that came back
in an error state, freeing the decoded data the surface never took ownership
of. SVGImageElement::render() already skips a null image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the three things loadImageResource() can get back for an `<image>`
element: nothing, a surface in an error state, and a usable surface. The first
case segfaults without the previous commit; the last one checks the image is
still drawn and that the decoded bytes stay attached to the surface.

Runs under `ctest`.

Co-Authored-By: Claude Opus 5 <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