fix(): Multiple SkCanvas methods crash or drop arguments for optional/null parameters (drawPatch, drawAtlas, drawImageRect)
Description
When using the imperative SkCanvas drawing API (canvas.drawPatch, canvas.drawAtlas, canvas.drawImageRect, and canvas.drawPoints), several methods crash with native segmentation faults (SIGSEGV), throw unhandled exceptions, or silently drop passed parameters when optional or nullable arguments are omitted or passed as null.
Specifically:
canvas.drawPatchcrashes withSIGSEGV(null pointer dereference) on Native, and throws on Web:- The TypeScript API declares
drawPatch(cubics, colors?, texs?, mode?, paint?). - On native (
JsiSkCanvas.h),auto paint = count >= 4 ? JsiSkPaint::fromValue(runtime, arguments[4]) : nullptr;checkscount >= 4to accessarguments[4]. When 4 arguments are passed (count == 4), index 4 is read out-of-bounds on the JSI argument array. - When
paintis omitted or passed asnull(count < 5),paintisnullptr. The code then calls_canvas->drawPatch(..., *paint), which unconditionally dereferences*paint, immediately crashing the app with a segmentation fault (SIGSEGV). - If
modeis omitted (count < 4),arguments[3].asNumber()throws an unhandled JS exception becausearguments[3]is undefined. - On Web (
JsiSkCanvas.ts), CanvasKit's underlying C++ binding takesconst SkPaint&, so Emscripten attempts to readpaint.Fd. Whenpaintis omitted or null, it throwsTypeError: Cannot read properties of undefined (reading 'Fd').
- The TypeScript API declares
canvas.drawAtlassilently ignoresblendModewhen 5 arguments are passed (whencolorsis omitted):- The TypeScript signature is
drawAtlas(atlas, srcs, dsts, paint, blendMode?, colors?, sampling?). - In
packages/skia/cpp/api/JsiSkCanvas.h:auto blendMode = count > 5 && !arguments[4].isUndefined() ? static_cast<SkBlendMode>(arguments[4].asNumber()) : SkBlendMode::kDstOver; blendModeis at index 4 (the 5th argument). If a developer callscanvas.drawAtlas(atlas, srcs, dsts, paint, BlendMode.SrcOver),countis 5. Because5 > 5evaluates tofalse, the passedblendModeis completely ignored and falls back toSkBlendMode::kDstOver. It is only respected if at least 6 arguments (i.e.colors) are also passed.
- The TypeScript signature is
canvas.drawImageRectthrows when optionalpaintis omitted or passed asnull:- The TypeScript signature declares
paint?: SkPaint | null. JsiSkCanvas.hunconditionally callsauto paint = JsiSkPaint::fromValue(runtime, arguments[3]);without checkingcount >= 4or whetherarguments[3]is null/undefined. Passingnullor omittingpaintthrowsValue is null, expected an Object.- On Web, passing
null/undefinedalso fails in Emscripten'stoWireTypeonundefined.Fd.
- The TypeScript signature declares
canvas.drawPointsthrows on empty points array instead of no-oping:JsiSkCanvas.hunconditionally throwsstd::invalid_argument("Points array must not be empty")ifpoints.empty(). In graphic routines (e.g. dynamic/particle rendering or empty buffers), drawing an empty array is standard behavior and a no-op across Skia and the Skia recorder, but inJsiSkCanvasit aborts execution.
React Native Skia Version
2.1.1+ (main branch)
React Native Version
0.81.5
Using New Architecture
- Enabled
Steps to Reproduce
- Call
canvas.drawPatch(cubics, colors)without passing the 5th argumentpaint:- On iOS / Android, the process terminates immediately with
EXC_BAD_ACCESS/SIGSEGVdue to*paintwherepaint == nullptr. - On Web, it throws
TypeError: Cannot read properties of undefined (reading 'Fd').
- On iOS / Android, the process terminates immediately with
- Call
canvas.drawAtlas(atlas, rects, transforms, paint, BlendMode.SrcOver):- On native,
count == 5. The conditioncount > 5evaluates to false, soBlendMode.SrcOveris dropped andkDstOveris drawn instead.
- On native,
- Call
canvas.drawImageRect(image, src, dest, null):- Throws
Value is null, expected an Object.
- Throws
- Call
canvas.drawPoints(PointMode.Points, [], paint):- Throws
Points array must not be empty.
- Throws
Snack, Code Example, Screenshot, or Link to Repository
Minimal reproduction code:
// 1. drawPatch crash
const surface = Skia.Surface.Make(100, 100);
const canvas = surface.getCanvas();
const cubics = Array.from({ length: 12 }, (_, i) => ({ x: i, y: i }));
const colors = [
Skia.Color("red"),
Skia.Color("green"),
Skia.Color("blue"),
Skia.Color("white"),
];
// Crashes with SIGSEGV on iOS/Android due to null pointer dereference (*paint)
canvas.drawPatch(cubics, colors);
// 2. drawAtlas dropped blendMode
const img = surface.makeImageSnapshot();
const paint = Skia.Paint();
// BlendMode.SrcOver is silently dropped and replaced with DstOver because count == 5
canvas.drawAtlas(
img,
[Skia.XYWHRect(0, 0, 8, 8)],
[Skia.RSXform(1, 0, 0, 0)],
paint,
BlendMode.SrcOver
);
// 3. drawImageRect with null paint throws
canvas.drawImageRect(img, Skia.XYWHRect(0, 0, 8, 8), Skia.XYWHRect(0, 0, 8, 8), null);
// 4. drawPoints empty array throws
canvas.drawPoints(PointMode.Points, [], paint);Proposed Fix
- In
JsiSkCanvas.h:- Guard
drawPatchparameters withcount >= 4andcount >= 5, provide defaultSkPaintfallback whenpaintisnullptrinstead of dereferencing*paint, and defaultblendModetocolors.empty() ? SkBlendMode::kSrcOver : SkBlendMode::kDstOver. - In
drawAtlas, updatecount > 5tocount >= 5 && !arguments[4].isNull() && !arguments[4].isUndefined(). - In
drawImageanddrawImageRect, guardarguments[3]withcount >= 4 && !arguments[3].isNull() && !arguments[3].isUndefined(). - In
drawPoints, return earlyif (points.empty()) return;.
- Guard
- In
JsiSkCanvas.ts(Web):- Provide fallback
CanvasKit.PaintindrawPatchanddrawImageRectwhenpaintis omitted/null to satisfy Emscripten's wire-type requirement. - Return early
if (points.length === 0) return;indrawPoints.
- Provide fallback
- In
Canvas.ts(Types):- Allow
paint?: SkPaint | nullacrossdrawImage,drawImageRect,drawPatch, andsaveLayer.
- Allow
Source: Shopify/react-native-skia