Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"@sentry/core": "10.47.0",
"@sentry/react-native": "8.7.0",
"@shopify/flash-list": "2.3.0",
"@shopify/react-native-skia": "^2.4.14",
"@shopify/react-native-skia": "^2.11.0",
"@ua/react-native-airship": "26.5.0",
"array.prototype.tosorted": "^1.1.4",
"awesome-phonenumber": "^5.4.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
diff --git a/node_modules/@shopify/react-native-skia/cpp/jsi/RuntimeAwareCache.h b/node_modules/@shopify/react-native-skia/cpp/jsi/RuntimeAwareCache.h
index 2b5f16b..f716161 100644
index 18d68d3..e7d9bde 100644
--- a/node_modules/@shopify/react-native-skia/cpp/jsi/RuntimeAwareCache.h
+++ b/node_modules/@shopify/react-native-skia/cpp/jsi/RuntimeAwareCache.h
@@ -3,6 +3,7 @@
#include <jsi/jsi.h>

#include <memory>
+#include <mutex>
#include <unordered_map>
#include <utility>

@@ -56,17 +57,37 @@ class RuntimeAwareCache : public BaseRuntimeAwareCache,
@@ -60,7 +60,18 @@ class RuntimeAwareCache : public BaseRuntimeAwareCache,

public:
void onRuntimeDestroyed(jsi::Runtime *rt) override {
- if (getMainJsRuntime() != rt) {
+ if (getMainJsRuntime() == rt) {
+ // Release the primary cache here, while the runtime is still valid, instead
+ // of in ~RuntimeAwareCache which may run later (main queue) after the
+ // runtime is freed -> UAF. See patches/@shopify/react-native-skia.
+ // runtime is freed -> UAF. Reset by move-assigning a fresh T: this is the
+ // same point in the runtime's lifecycle at which the secondary branch below
+ // erases its entries. See patches/@shopify/react-native-skia.
+ std::lock_guard<std::mutex> lock(_primaryCacheMutex);
+ if (!_primaryCacheHandled) {
+ _primaryCache.clear();
+ _primaryCache = T{};
+ _primaryCacheHandled = true;
+ }
+ } else {
// We are removing a secondary runtime
_secondaryRuntimeCaches.erase(rt);
}
// We are removing a secondary runtime. This is invoked by
// RuntimeLifecycleMonitor on the destroyed runtime's thread, which may
// run concurrently with get() on another runtime's thread.
@@ -70,6 +81,17 @@ public:
}

~RuntimeAwareCache() {
Expand All @@ -45,11 +40,7 @@ index 2b5f16b..f716161 100644
for (auto &cache : _secondaryRuntimeCaches) {
RuntimeLifecycleMonitor::removeListener(
*static_cast<jsi::Runtime *>(cache.first), this);
}
}

T &get(jsi::Runtime &rt) {
@@ -74,6 +93,13 @@ public:
@@ -81,6 +103,13 @@ public:
// to avoid us having to lookup by runtime for caches that only has a single
// runtime
if (getMainJsRuntime() == &rt) {
Expand All @@ -62,11 +53,11 @@ index 2b5f16b..f716161 100644
+ }
return _primaryCache;
} else {
if (_secondaryRuntimeCaches.count(&rt) == 0) {
@@ -94,6 +120,10 @@ public:
}
// Guard the secondary map: it can be mutated concurrently by get()
@@ -110,6 +139,10 @@ public:

private:
std::mutex _secondaryCachesMutex;
+ // Declared before _primaryCache so it outlives it during member destruction.
+ std::mutex _primaryCacheMutex;
+ jsi::Runtime *_primaryRuntime = nullptr;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
diff --git a/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js b/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js
index 407ab37..cbd08b2 100644
--- a/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js
+++ b/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js
@@ -30,19 +30,29 @@ class WebGLRenderer {
_defineProperty(this, "grContext", null);
_defineProperty(this, "contextHandle", 0);
_defineProperty(this, "pd", 1);
+ _defineProperty(this, "isSoftware", false);
+ // Context creation fails when the browser cannot give us WebGL2 - hardware acceleration disabled,
+ // GPU blocklisted, or the per-page live context limit exhausted. Throwing here tears down the view
+ // tree, and the sibling throw in onResize escapes a ResizeObserver callback where no try/catch or
+ // React error boundary can reach it. Fall back to CanvasKit's software surface instead, matching how
+ // renderPictureToSurface below already treats a failed WebGL surface as recoverable.
this.contextHandle = CanvasKit.GetWebGLContext(canvas);
- if (!this.contextHandle) {
- throw new Error("Could not create a WebGL context");
- }
- this.grContext = CanvasKit.MakeWebGLContext(this.contextHandle);
- if (!this.grContext) {
- CanvasKit.deleteContext(this.contextHandle);
- this.contextHandle = 0;
- throw new Error("Could not create a graphics context");
+ if (this.contextHandle) {
+ this.grContext = CanvasKit.MakeWebGLContext(this.contextHandle);
}
- const ctx = canvas.getContext("webgl2");
- if (ctx) {
- ctx.drawingBufferColorSpace = "display-p3";
+ if (this.grContext) {
+ const ctx = canvas.getContext("webgl2");
+ if (ctx) {
+ ctx.drawingBufferColorSpace = "display-p3";
+ }
+ } else {
+ if (this.contextHandle) {
+ CanvasKit.deleteContext(this.contextHandle);
+ this.contextHandle = 0;
+ }
+ // Let onResize try the software surface. It can only succeed when GetWebGLContext failed, since a
+ // canvas that already holds a WebGL context can never hand back a 2D one.
+ this.isSoftware = true;
}
this.onResize();
}
@@ -61,7 +71,7 @@ class WebGLRenderer {
const {
canvas
} = this;
- if (!this.grContext) {
+ if (!this.grContext && !this.isSoftware) {
return;
}
this.pd = window.devicePixelRatio;
@@ -76,9 +86,30 @@ class WebGLRenderer {
}
// Reuse the existing WebGL context and GrDirectContext: only the surface
// needs to be recreated when the canvas is resized.
- const surface = CanvasKit.MakeOnScreenGLSurface(this.grContext, canvas.width, canvas.height, CanvasKit.ColorSpace.SRGB);
+ let surface = this.grContext ? CanvasKit.MakeOnScreenGLSurface(this.grContext, canvas.width, canvas.height, CanvasKit.ColorSpace.SRGB) : null;
if (!surface) {
- throw new Error("Could not create surface");
+ try {
+ // MakeSWCanvasSurface only stores the canvas and calls getContext("2d") later, when the surface
+ // is flushed. A canvas that already holds a WebGL context can never return a 2D context, so
+ // creating the surface anyway would defer the failure into an uncatchable putImageData error.
+ surface = canvas.getContext("2d") ? CanvasKit.MakeSWCanvasSurface(canvas) : null;
+ } catch (e) {
+ surface = null;
+ }
+ this.isSoftware = surface !== null;
+ }
+ if (!surface) {
+ this.surface = null;
+ // Let the host application know the chart cannot render, so it can show a fallback instead of a
+ // blank canvas. Without a listener this is a no-op. Deferred a frame so a listener attached in a
+ // React effect is registered before the event fires - the renderer is built in a layout effect,
+ // which runs first.
+ requestAnimationFrame(() => {
+ canvas.dispatchEvent(new CustomEvent("skia-surface-unavailable", {
+ bubbles: true
+ }));
+ });
+ return;
}
this.surface = new _JsiSkSurface.JsiSkSurface(CanvasKit, surface);
}
diff --git a/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js b/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js
index 7a9ea19..c24ab14 100644
--- a/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js
+++ b/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js
@@ -24,19 +24,29 @@ class WebGLRenderer {
_defineProperty(this, "grContext", null);
_defineProperty(this, "contextHandle", 0);
_defineProperty(this, "pd", 1);
+ _defineProperty(this, "isSoftware", false);
+ // Context creation fails when the browser cannot give us WebGL2 - hardware acceleration disabled,
+ // GPU blocklisted, or the per-page live context limit exhausted. Throwing here tears down the view
+ // tree, and the sibling throw in onResize escapes a ResizeObserver callback where no try/catch or
+ // React error boundary can reach it. Fall back to CanvasKit's software surface instead, matching how
+ // renderPictureToSurface below already treats a failed WebGL surface as recoverable.
this.contextHandle = CanvasKit.GetWebGLContext(canvas);
- if (!this.contextHandle) {
- throw new Error("Could not create a WebGL context");
- }
- this.grContext = CanvasKit.MakeWebGLContext(this.contextHandle);
- if (!this.grContext) {
- CanvasKit.deleteContext(this.contextHandle);
- this.contextHandle = 0;
- throw new Error("Could not create a graphics context");
+ if (this.contextHandle) {
+ this.grContext = CanvasKit.MakeWebGLContext(this.contextHandle);
}
- const ctx = canvas.getContext("webgl2");
- if (ctx) {
- ctx.drawingBufferColorSpace = "display-p3";
+ if (this.grContext) {
+ const ctx = canvas.getContext("webgl2");
+ if (ctx) {
+ ctx.drawingBufferColorSpace = "display-p3";
+ }
+ } else {
+ if (this.contextHandle) {
+ CanvasKit.deleteContext(this.contextHandle);
+ this.contextHandle = 0;
+ }
+ // Let onResize try the software surface. It can only succeed when GetWebGLContext failed, since a
+ // canvas that already holds a WebGL context can never hand back a 2D one.
+ this.isSoftware = true;
}
this.onResize();
}
@@ -55,7 +65,7 @@ class WebGLRenderer {
const {
canvas
} = this;
- if (!this.grContext) {
+ if (!this.grContext && !this.isSoftware) {
return;
}
this.pd = window.devicePixelRatio;
@@ -70,9 +80,30 @@ class WebGLRenderer {
}
// Reuse the existing WebGL context and GrDirectContext: only the surface
// needs to be recreated when the canvas is resized.
- const surface = CanvasKit.MakeOnScreenGLSurface(this.grContext, canvas.width, canvas.height, CanvasKit.ColorSpace.SRGB);
+ let surface = this.grContext ? CanvasKit.MakeOnScreenGLSurface(this.grContext, canvas.width, canvas.height, CanvasKit.ColorSpace.SRGB) : null;
if (!surface) {
- throw new Error("Could not create surface");
+ try {
+ // MakeSWCanvasSurface only stores the canvas and calls getContext("2d") later, when the surface
+ // is flushed. A canvas that already holds a WebGL context can never return a 2D context, so
+ // creating the surface anyway would defer the failure into an uncatchable putImageData error.
+ surface = canvas.getContext("2d") ? CanvasKit.MakeSWCanvasSurface(canvas) : null;
+ } catch (e) {
+ surface = null;
+ }
+ this.isSoftware = surface !== null;
+ }
+ if (!surface) {
+ this.surface = null;
+ // Let the host application know the chart cannot render, so it can show a fallback instead of a
+ // blank canvas. Without a listener this is a no-op. Deferred a frame so a listener attached in a
+ // React effect is registered before the event fires - the renderer is built in a layout effect,
+ // which runs first.
+ requestAnimationFrame(() => {
+ canvas.dispatchEvent(new CustomEvent("skia-surface-unavailable", {
+ bubbles: true
+ }));
+ });
+ return;
}
this.surface = new JsiSkSurface(CanvasKit, surface);
}
Loading
Loading