Shopify / Shopify/react-native-skia

fix(🎨): Multiple SkCanvas methods crash or drop arguments for optional/null parameters (drawPatch, drawAtlas, drawImageRect)

Open
#4,062 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
8.6k
Forks
647
Avg merge
1d 17h
Merged PRs (30d)
35

Description

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:

  1. canvas.drawPatch crashes with SIGSEGV (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; checks count >= 4 to access arguments[4]. When 4 arguments are passed (count == 4), index 4 is read out-of-bounds on the JSI argument array.
    • When paint is omitted or passed as null (count < 5), paint is nullptr. The code then calls _canvas->drawPatch(..., *paint), which unconditionally dereferences *paint, immediately crashing the app with a segmentation fault (SIGSEGV).
    • If mode is omitted (count < 4), arguments[3].asNumber() throws an unhandled JS exception because arguments[3] is undefined.
    • On Web (JsiSkCanvas.ts), CanvasKit's underlying C++ binding takes const SkPaint&, so Emscripten attempts to read paint.Fd. When paint is omitted or null, it throws TypeError: Cannot read properties of undefined (reading 'Fd').
  2. canvas.drawAtlas silently ignores blendMode when 5 arguments are passed (when colors is 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;
      
    • blendMode is at index 4 (the 5th argument). If a developer calls canvas.drawAtlas(atlas, srcs, dsts, paint, BlendMode.SrcOver), count is 5. Because 5 > 5 evaluates to false, the passed blendMode is completely ignored and falls back to SkBlendMode::kDstOver. It is only respected if at least 6 arguments (i.e. colors) are also passed.
  3. canvas.drawImageRect throws when optional paint is omitted or passed as null:

    • The TypeScript signature declares paint?: SkPaint | null.
    • JsiSkCanvas.h unconditionally calls auto paint = JsiSkPaint::fromValue(runtime, arguments[3]); without checking count >= 4 or whether arguments[3] is null/undefined. Passing null or omitting paint throws Value is null, expected an Object.
    • On Web, passing null/undefined also fails in Emscripten's toWireType on undefined.Fd.
  4. canvas.drawPoints throws on empty points array instead of no-oping:

    • JsiSkCanvas.h unconditionally throws std::invalid_argument("Points array must not be empty") if points.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 in JsiSkCanvas it aborts execution.
React Native Skia Version

2.1.1+ (main branch)

React Native Version

0.81.5

Using New Architecture
  • Enabled
Steps to Reproduce
  1. Call canvas.drawPatch(cubics, colors) without passing the 5th argument paint:
    • On iOS / Android, the process terminates immediately with EXC_BAD_ACCESS / SIGSEGV due to *paint where paint == nullptr.
    • On Web, it throws TypeError: Cannot read properties of undefined (reading 'Fd').
  2. Call canvas.drawAtlas(atlas, rects, transforms, paint, BlendMode.SrcOver):
    • On native, count == 5. The condition count > 5 evaluates to false, so BlendMode.SrcOver is dropped and kDstOver is drawn instead.
  3. Call canvas.drawImageRect(image, src, dest, null):
    • Throws Value is null, expected an Object.
  4. Call canvas.drawPoints(PointMode.Points, [], paint):
    • Throws Points array must not be empty.
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 drawPatch parameters with count >= 4 and count >= 5, provide default SkPaint fallback when paint is nullptr instead of dereferencing *paint, and default blendMode to colors.empty() ? SkBlendMode::kSrcOver : SkBlendMode::kDstOver.
    • In drawAtlas, update count > 5 to count >= 5 && !arguments[4].isNull() && !arguments[4].isUndefined().
    • In drawImage and drawImageRect, guard arguments[3] with count >= 4 && !arguments[3].isNull() && !arguments[3].isUndefined().
    • In drawPoints, return early if (points.empty()) return;.
  • In JsiSkCanvas.ts (Web):
    • Provide fallback CanvasKit.Paint in drawPatch and drawImageRect when paint is omitted/null to satisfy Emscripten's wire-type requirement.
    • Return early if (points.length === 0) return; in drawPoints.
  • In Canvas.ts (Types):
    • Allow paint?: SkPaint | null across drawImage, drawImageRect, drawPatch, and saveLayer.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the affected entry points in packages/skia/cpp/api/JsiSkCanvas.h, JsiSkCanvas.ts, and Canvas.ts, then run the four reproduction calls from the issue on native and Web. Done means omitted or null paints no longer crash, drawAtlas preserves its fifth-argument blend mode, and empty drawPoints calls no-op consistently.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, react-native, typescript
Domain
api, computer-graphics, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.