Skip to content

[Skia][Android] Image shaders with toImageSync images abort the app in GrResourceCache (SIGTRAP) #195888

Closed

Description

@doutatsu

Steps to reproduce

  1. flutter create skia_repro, and replace lib/main.dart with the code below. It uses no package.
  2. Run it in profile mode on an Android device that uses the Skia backend, for example the API 24 emulator below: flutter run --profile.
  3. 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:10

From 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:5
F/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:27

Flutter 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

  1. added
    c: crashStack traces logged to the console
    platform-androidAndroid applications specifically
    engineflutter/engine related. See also e: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    on Nov 3, 2026
  2. mbcorona commented on Nov 3, 2026

    @mbcorona
    Member

    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.

  3. added
    P1High-priority issues at the top of the work list
    on Nov 9, 2026
  4. b-luk commented on Nov 13, 2026

    @b-luk
    Contributor

    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.

    #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.

  5. doutatsu commented on Nov 13, 2026

    @doutatsu
    Author

    Thanks for the fix in #196473, and for reading the report so closely.

    You're right: the code sample reproduces only the UI-thread Equals path. 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 the Equals path 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listc: crashStack traces logged to the consoleengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions