Repository navigation
Description
Steps to reproduce
flutter create skia_repro, and replacelib/main.dartwith the code below. It uses no package.- Run it in profile mode on an Android device that uses the Skia backend, for example the API 24 emulator below:
flutter run --profile. - Leave it running for 2 minutes. Each frame records a new picture of 300 ovals in a post-frame callback, each oval with an image shader of one of about 36 tiles made with
toImageSync. The oldest tiles are disposed as new ones come, like a cache.
- Flutter 3.47.6, stable. Framework 5fc3468, engine b8c8d3d8d5 (revision 692136c), Dart 3.13.5.
- Android emulator, API 24 (Android 7.0, arm64 system image), 1 GB RAM, 2 cores, booted with
-prop ro.config.low_ram=true. The app runs on the Skia GL backend. Profile build.
On that emulator, the code sample aborted in 1 of 3 runs of 2 minutes, on the raster thread (stack in Logs). The same app with the tiles from the async toImage (the _tile future adds the tile when it is ready) ran 3 runs of 2 minutes with no crash. The repro's crash rate is low (1 in 3 runs), so the counts in "What we tried" below, from a larger app, are the stronger evidence.
Expected results
The app draws the image shaders without a crash, as it does on Impeller.
Actual results
The app aborts with Fatal signal 5 (SIGTRAP), code 1.
On the Skia backend on Android, an app that draws many paths with ImageShader paints, whose images come from Picture.toImageSync, aborts with SIGTRAP. The abort is a bounds check in Skia's GrResourceCache, on the UI (platform) thread or on the raster thread.
One path is clear from the UI-thread stack in Logs. When two draws in a row use image shaders with two different toImageSync images, DisplayListBuilder::SetAttributesFromPaint compares the two color sources on the UI thread. DlImage::Equals calls skia_image() on each image. For a DlDeferredImageGPUSkia, that is ImageWrapper::CreateSkiaImage(), which calls SkImages::BorrowTextureFrom with the raster thread's GrDirectContext. The temporary SkImage is then destroyed on the UI thread, and its GrTextureProxy removes a resource from the raster context's GrResourceCache while the raster thread uses that cache.
CreateSkiaImage has FML_DCHECK(raster_task_runner_->RunsTasksOnCurrentThread()), but DCHECKs are compiled out of the release and profile engines, so nothing stops the call. DlDeferredImageGPUSkia::isUIThreadSafe() returns true.
Impeller is not affected: asSkiaImage() is null there, and Equals falls back to pointer equality.
What we tried
All on the same emulator, the same harness (a fast scroll through a long list of pictures, each recorded in short slices in post-frame callbacks, and drawn into image strips with the async toImage):
| Variant | Runs | Crashes |
|---|---|---|
Tiles from toImageSync, used in ImageShader paints |
5 | 4 |
| The same, with a zero-size draw with a plain paint after each image-shader draw, so the builder never compares two image color sources | 3 | 2 (both on the raster thread; the UI-thread stack in Logs went away) |
| The same tiles, never disposed | 1 | 1 |
| Plain colours instead of the image shaders | 4 | 0 |
Tiles from the async toImage instead |
9 | 0 |
So the UI-thread Equals path is one way in, but not the only one: with it closed, the raster thread still found its resource cache broken. We could not find the second path by reading the engine. Every crash needed toImageSync images in image shaders.
The same app recorded these pictures inside the build phase of a frame before, and ran about 10 times with no crash. The crash came when the recording moved to post-frame callbacks, which run right after a frame goes to the raster thread, while the raster thread is busy. So it looks like a race between the UI thread and the raster thread.
Possible fix
DlImage::Equals (or an override in DlDeferredImageGPUSkia) could compare deferred images without making a Skia image, for example by the identity of their ImageWrapper. More generally, skia_image() on a deferred image could refuse to run off the raster thread in release builds too, not only under a DCHECK.
Workaround
Make the images with the async Picture.toImage, which gives an ordinary image.
Code sample
Code sample
// Repro: image shaders whose images come from Picture.toImageSync, drawn
// back to back in pictures recorded in post-frame callbacks, abort the app on
// the Skia backend (GrResourceCache bounds check, SIGTRAP).
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(const MaterialApp(home: Repro()));
class Repro extends StatefulWidget {
const Repro({super.key});
@override
State<Repro> createState() => _ReproState();
}
class _ReproState extends State<Repro> with SingleTickerProviderStateMixin {
// Keeps a frame going to the raster thread on every vsync.
late final _ticker = createTicker((_) => setState(() {}))..start();
final _tiles = <ui.Image>[];
ui.Picture? _picture;
var _round = 0;
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback(_record);
}
@override
void dispose() {
_ticker.dispose();
super.dispose();
}
/// A 64 px tile in its own colour, with a few lines, from toImageSync.
ui.Image _tile(int i) {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder)
..drawRect(
const Rect.fromLTWH(0, 0, 64, 64),
Paint()..color = Color(0xFF000000 | (i * 2654435761) & 0xFFFFFF),
);
final line = Paint()..color = const Color(0x33000000);
for (var k = 0; k < 32; k++) {
canvas.drawLine(Offset(k * 2.0, 0), Offset(k * 2.0, 64), line);
}
final picture = recorder.endRecording();
final image = picture.toImageSync(64, 64);
picture.dispose();
return image;
}
/// After each frame: a few new tiles (the oldest go, as in a cache), and a
/// new picture of 300 paths, each with an image shader of another tile.
void _record(Duration _) {
for (var k = 0; k < 6; k++) {
_tiles.add(_tile(_round * 6 + k));
}
while (_tiles.length > 36) {
_tiles.removeAt(0).dispose();
}
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
for (var n = 0; n < 300; n++) {
canvas.drawPath(
Path()
..addOval(
Rect.fromLTWH((n * 37) % 320.0, (n * 53 + _round) % 700.0, 90, 60),
),
Paint()
..shader = ImageShader(
_tiles[n % _tiles.length],
TileMode.repeated,
TileMode.repeated,
Matrix4.identity().storage,
),
);
}
_picture?.dispose();
_picture = recorder.endRecording();
_round++;
SchedulerBinding.instance.addPostFrameCallback(_record);
}
@override
Widget build(BuildContext context) =>
CustomPaint(painter: _Painter(_picture, _round), size: Size.infinite);
}
class _Painter extends CustomPainter {
_Painter(this.picture, this.round);
final ui.Picture? picture;
final int round;
@override
void paint(Canvas canvas, Size size) {
if (picture case final picture?) canvas.drawPicture(picture);
}
@override
bool shouldRepaint(_Painter old) => old.round != round;
}Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
From the code sample above (raster thread):
Fatal signal 5 (SIGTRAP), code 1 in tid 2091 (1.raster)
#00 SkTDPQueue<GrGpuResource*, &GrResourceCache::CompareTimestamp(GrGpuResource* const&, GrGpuResource* const&), &GrResourceCache::AccessResourceIndex(GrGpuResource* const&)>::remove(GrGpuResource*) s
#01 GrResourceCache::removeResource(GrGpuResource*) skia/src/gpu/ganesh/GrResourceCache.cpp:115:25
#02 GrResourceCache::ResourceAccess::removeResource(GrGpuResource*) skia/src/gpu/ganesh/GrResourceCache.h:425:60
#03 sk_sp<GrSurface>::operator=(std::nullptr_t) skia/include/core/SkRefCnt.h:260:49
#04 GrTextureRenderTargetProxy::~GrTextureRenderTargetProxy() skia/src/gpu/ganesh/GrTextureRenderTargetProxy.h:44:7
#05 GrTextureRenderTargetProxy::~GrTextureRenderTargetProxy() skia/src/gpu/ganesh/GrTextureRenderTargetProxy.h:44:7
#06 sk_sp<GrSurfaceProxy>::~sk_sp() skia/include/core/SkRefCnt.h:256:9
#07 skgpu::ganesh::OpsTask::~OpsTask() skia/src/gpu/ganesh/ops/OpsTask.cpp:431:21
#08 skia_private::TArray<sk_sp<skgpu::ganesh::AtlasRenderTask>, true>::destroyAll() skia/include/private/SkTArray.h:642:26
#09 GrDrawingManager::removeRenderTasks() skia/src/gpu/ganesh/GrDrawingManager.cpp:0:27
#10 GrDrawingManager::flush(SkSpan<GrSurfaceProxy*>, SkSurfaces::BackendSurfaceAccess, GrFlushInfo const&, skgpu::MutableTextureState const*) skia/src/gpu/ganesh/GrDrawingManager.cpp:211:11
#11 GrDirectContext::flush(GrFlushInfo const&) skia/src/gpu/ganesh/GrDirectContext.cpp:446:36
#12 flutter::GPUSurfaceGLSkia::AcquireFrame(impeller::TSize<int> const&)::$_0::operator()(flutter::SurfaceFrame const&, flutter::DlCanvas*) const shell/gpu/gpu_surface_gl_skia.cc:255:27
#13 std::__fl::__function::__value_func<bool (flutter::SurfaceFrame&, flutter::DlCanvas*)>::operator()[abi:nn210000](flutter::SurfaceFrame&, flutter::DlCanvas*&&) const third_party/libcxx/include/__fu
#14 flutter::Rasterizer::DrawToSurfaceUnsafe(long, flutter::LayerTree&, float, std::__fl::optional<fml::TimePoint>) shell/common/rasterizer.cc:835:14
#15 flutter::Rasterizer::DrawToSurfaces(flutter::FrameTimingsRecorder&, std::__fl::vector<std::__fl::unique_ptr<flutter::LayerTreeTask, std::__fl::default_delete<flutter::LayerTreeTask>>, std::__fl::a
#16 flutter::Rasterizer::DoDraw(std::__fl::unique_ptr<flutter::FrameTimingsRecorder, std::__fl::default_delete<flutter::FrameTimingsRecorder>>, std::__fl::vector<std::__fl::unique_ptr<flutter::LayerTr
#17 std::__fl::__function::__value_func<void (std::__fl::unique_ptr<flutter::FrameItem, std::__fl::default_delete<flutter::FrameItem>>)>::operator()[abi:nn210000](std::__fl::unique_ptr<flutter::FrameI
#18 flutter::Shell::OnAnimatorDraw(std::__fl::shared_ptr<flutter::Pipeline<flutter::FrameItem>>)::$_0::operator()() shell/common/shell.cc:1423:25
#19 std::__fl::function<void ()>::operator()() const third_party/libcxx/include/__functional/function.h:995:10From the larger app in "What we tried", on the UI thread (one run), symbolised with the engine's android-arm64-profile symbols:
F/libc ( 2279): Fatal signal 5 (SIGTRAP), code 1 in tid 2279 (main)
#00 SkTDPQueue<GrGpuResource*, &GrResourceCache::CompareTimestamp(GrGpuResource* const&, GrGpuResource* const&), &GrResourceCache::AccessResourceIndex(GrGpuResource* const&)>::remove(GrGpuResource*) skia/src/core/SkTDPQueue.h:0:0
#01 GrResourceCache::removeResource(GrGpuResource*) skia/src/gpu/ganesh/GrResourceCache.cpp:115:25
#02 GrResourceCache::ResourceAccess::removeResource(GrGpuResource*) skia/src/gpu/ganesh/GrResourceCache.h:425:60
#03 sk_sp<GrSurface>::operator=(std::nullptr_t) skia/include/core/SkRefCnt.h:260:49
#04 GrTextureProxy::~GrTextureProxy() skia/src/gpu/ganesh/GrTextureProxy.cpp:105:35
#05 GrTextureProxy::~GrTextureProxy() skia/src/gpu/ganesh/GrTextureProxy.cpp:105:35
#06 sk_sp<GrSurfaceProxy>::~sk_sp() skia/include/core/SkRefCnt.h:256:9
#07 SkImage_Ganesh::~SkImage_Ganesh() skia/src/gpu/ganesh/image/SkImage_Ganesh.cpp:244:33
#08 flutter::DlImage::Equals(flutter::DlImage const*) const display_list/image/dl_image.cc:45:3
#09 flutter::DlImage::Equals(sk_sp<flutter::DlImage const> const&) const display_list/image/dl_image.h:148:12
#10 bool flutter::NotEquals<flutter::DlColorSource, flutter::DlColorSource>(flutter::DlColorSource const*, flutter::DlColorSource const*) display_list/utils/dl_comparable.h:72:11
#11 flutter::DisplayListBuilder::SetAttributesFromPaint(flutter::DlPaint const&, flutter::DisplayListAttributeFlags) display_list/dl_builder.cc:477:5
#12 flutter::DisplayListBuilder::DrawPath(flutter::DlPath const&, flutter::DlPaint const&) display_list/dl_builder.cc:1429:3
#13 flutter::Canvas::drawPath(flutter::CanvasPath const*, _Dart_Handle*, _Dart_Handle*) lib/ui/painting/canvas.cc:391:16
#14 libapp.so (app AOT code)From the larger app, on the raster thread (other runs):
F/libc ( 1955): Fatal signal 5 (SIGTRAP), code 1 in tid 1992 (1.raster)
#00 int sk_collection_check_bounds<int>(int, int) third_party/skia/include/private/SkAssert.h:153:13
#01 GrResourceCache::notifyARefCntReachedZero(GrGpuResource*, GrIORef<GrGpuResource>::LastRemovedRef) third_party/skia/src/gpu/ganesh/GrResourceCache.cpp:345:11
#02 GrSimpleMesh::~GrSimpleMesh() third_party/skia/include/core/SkRefCnt.h:0:9
#03 decltype(fp(nullptr)) SkArenaAlloc::make<GrSimpleMesh* SkArenaAlloc::make<GrSimpleMesh>()::'lambda'(void*)>(GrSimpleMesh&&)::'lambda'(char*)::operator()(char*) const third_party/skia/src/core/SkArenaAlloc.h:141:34
#04 SkArenaAlloc::RunDtorsOnBlock(char*) third_party/skia/src/core/SkArenaAlloc.cpp:60:21
#05 SkArenaAlloc::NextBlock(char*) third_party/skia/src/core/SkArenaAlloc.cpp:68:5
#06 SkArenaAlloc::RunDtorsOnBlock(char*) third_party/skia/src/core/SkArenaAlloc.cpp:60:21
#07 SkArenaAllocWithReset::reset() third_party/skia/src/core/SkArenaAlloc.cpp:158:11
#08 GrOpFlushState::reset() third_party/skia/src/gpu/ganesh/GrOpFlushState.cpp:115:12
#09 GrDrawingManager::executeRenderTasks(GrOpFlushState*) third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp:306:17
#10 GrDirectContext::flush(GrFlushInfo const&) third_party/skia/src/gpu/ganesh/GrDirectContext.cpp:446:36
#11 flutter::GPUSurfaceGLSkia::AcquireFrame(impeller::TSize<int> const&)::$_0::operator()(flutter::SurfaceFrame const&, flutter::DlCanvas*) const shell/gpu/gpu_surface_gl_skia.cc:255:27
#12 std::__fl::__function::__value_func<bool (flutter::SurfaceFrame&, flutter::DlCanvas*)>::operator()[abi:nn210000](flutter::SurfaceFrame&, flutter::DlCanvas*&&) const third_party/libcxx/include/__functional/function.h:436:12
#13 flutter::Rasterizer::DrawToSurfaceUnsafe(long, flutter::LayerTree&, float, std::__fl::optional<fml::TimePoint>) shell/common/rasterizer.cc:835:14
#14 flutter::Rasterizer::DrawToSurfaces(flutter::FrameTimingsRecorder&, std::__fl::vector<std::__fl::unique_ptr<flutter::LayerTreeTask, std::__fl::default_delete<flutter::LayerTreeTask>>, std::__fl::allocator<std::__fl::unique_ptr
#15 flutter::Rasterizer::DoDraw(std::__fl::unique_ptr<flutter::FrameTimingsRecorder, std::__fl::default_delete<flutter::FrameTimingsRecorder>>, std::__fl::vector<std::__fl::unique_ptr<flutter::LayerTreeTask, std::__fl::default_del
#16 std::__fl::__function::__value_func<void (std::__fl::unique_ptr<flutter::FrameItem, std::__fl::default_delete<flutter::FrameItem>>)>::operator()[abi:nn210000](std::__fl::unique_ptr<flutter::FrameItem, std::__fl::default_delete
#17 flutter::Shell::OnAnimatorDraw(std::__fl::shared_ptr<flutter::Pipeline<flutter::FrameItem>>)::$_0::operator()() shell/common/shell.cc:1423:25
#18 std::__fl::function<void ()>::operator()() const third_party/libcxx/include/__functional/function.h:995:10
#19 fml::MessageLoopAndroid::OnEventFired() fml/platform/android/message_loop_android.cc:91:5F/libc ( 2274): Fatal signal 5 (SIGTRAP), code 1 in tid 2317 (1.raster)
#00 int sk_collection_check_bounds<int>(int, int) third_party/skia/include/private/SkAssert.h:153:13
#01 GrResourceCache::notifyARefCntReachedZero(GrGpuResource*, GrIORef<GrGpuResource>::LastRemovedRef) third_party/skia/src/gpu/ganesh/GrResourceCache.cpp:345:11
#02 sk_sp<GrSurface>::operator=(std::nullptr_t) third_party/skia/include/core/SkRefCnt.h:260:49
#03 GrTextureRenderTargetProxy::~GrTextureRenderTargetProxy() third_party/skia/src/gpu/ganesh/GrTextureRenderTargetProxy.h:44:7
#04 GrTextureRenderTargetProxy::~GrTextureRenderTargetProxy() third_party/skia/src/gpu/ganesh/GrTextureRenderTargetProxy.h:44:7
#05 sk_sp<GrSurfaceProxy>::~sk_sp() third_party/skia/include/core/SkRefCnt.h:256:9
#06 SkImage_Ganesh::~SkImage_Ganesh() third_party/skia/src/gpu/ganesh/image/SkImage_Ganesh.cpp:244:33
#07 flutter::DlImageSkia::~DlImageSkia() display_list/image/dl_image_skia.cc:28:27
#08 flutter::RasterCacheResult::~RasterCacheResult() flow/raster_cache.h:37:40
#09 std::__fl::pair<flutter::RasterCacheKey const, flutter::RasterCache::Entry>::~pair() third_party/libcxx/include/__memory/unique_ptr.h:0:9
#10 void std::__fl::__destroy_at[abi:nn210000]<std::__fl::pair<flutter::RasterCacheKey const, flutter::RasterCache::Entry>, 0>(std::__fl::pair<flutter::RasterCacheKey const, flutter::RasterCache::Entr
#11 std::__fl::__hash_table<std::__fl::__hash_value_type<flutter::RasterCacheKey, flutter::RasterCache::Entry>, std::__fl::__unordered_map_hasher<flutter::RasterCacheKey, std::__fl::__hash_value_type<
#12 flutter::CompositorContext::ScopedFrame::PaintLayerTreeSkia(flutter::LayerTree&, std::__fl::optional<impeller::TRect<float>>, bool, bool) flow/compositor_context.cc:180:14
#13 flutter::Rasterizer::DrawToSurfaceUnsafe(long, flutter::LayerTree&, float, std::__fl::optional<fml::TimePoint>) shell/common/rasterizer.cc:811:27Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.47.6, on macOS 26.6.2 25G83 darwin-arm64, locale en-GB) [488ms]
• Flutter version 3.47.6 on channel stable at /opt/homebrew/share/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 5fc346839b (5 weeks ago), 2026-09-30 15:02:49 -0700
• Engine revision 692136cb65
• Dart version 3.13.5
• DevTools version 2.60.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [2.2s]
• Android SDK at /Users/doutatsu/Library/Android/sdk
• Emulator version 37.2.12.0 (build_id 16428233) (CL:N/A)
• Platform android-36, build-tools 36.1.0
• Java binary at: /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment Homebrew (build 17.0.20.1+0)
• All Android licenses accepted.
[✓] Chrome - develop for the web [130ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [10.8s]
• macOS (desktop) • macos • darwin-arm64 • macOS 26.6.2 25G83 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 156.0.8078.80
[✓] Network resources [922ms]
• All expected network resources are available.
(Xcode and iOS left out: this bug is on Android.)
Activity
mbcorona commentedon Nov 3, 2026
Thanks @doutatsu for the repro.
DlDeferredImageGPUSkia.isUIThreadSafe() returns true, but skia_image() needs the raster
thread and is guarded only by an FML_DCHECK that's compiled out in profile/release.
Skia only, Impeller is fine.
Routing to the engine team for investigation.
b-luk commentedon Nov 13, 2026
#196473 fixes the UI-thread Equals path. I was not able to find a repro case for your reported second path, and without a repro we can't come up with a fix for it. If you're able to find a repro for this, please file another issue.
doutatsu commentedon Nov 13, 2026
Thanks for the fix in #196473, and for reading the report so closely.
You're right: the code sample reproduces only the UI-thread
Equalspath. Our evidence for the second path, on the raster thread, came from our full app, not from a standalone sample. In that app, a guard that closed theEqualspath still crashed in 2 of 3 runs.Our app now makes these images with the async
Picture.toImage, so it no longer hits either path. I'll look at building a standalone repro for the second path later. If I get one, I'll file a new issue, as you suggest.Don't call `skia_image()` for raster-owned images in `DlImage::Equals` (