Skip to content

[Impeller][GLES] Image.toByteData after Picture.toImage: SIGSEGV in the GL driver under ConfigureFBO, or SIGABRT on FML_CHECK in BlitPassGLES::EncodeCommands #195319

Description

@WessimBetclic

Steps to reproduce

Production crash, no local reproduction yet: we do not own one of the affected phones. The analysis below comes from symbolicated Crashlytics NDK stacks (engine symbols for revision 06a2e2a110089dff50fe635cffd2a61e1b24fbcd, Flutter 3.47.3 stable) and from reading the engine source at that revision.

What our app does, once per route change, once on return to the foreground (after AppLifecycleState.resumed), and once on a theme change:

  1. Replay the layer of a RepaintBoundary into a ui.Picture (a PictureRecorder plus canvas.drawPicture of the layer tree's pictures, no Scene).
  2. picture.toImage(w, h) for a 16 pixel wide strip of the status bar area, so a 16x1 or 16x2 image.
  3. image.toByteData() (rawRgba), then image.dispose().

The readback is skipped while the app is not resumed, and waits up to 500 ms for scheduled frames to settle.

On devices where Impeller runs on OpenGL ES, step 3 kills the process in two ways. Both are inside the deferred BlitCopyTextureToBufferCommandGLES that ImageEncodingImpeller::ConvertDlImageToSkImage queues (pass->AddCopy(texture, buffer); pass->EncodeCommands();, image_encoding_impeller.cc:207-208) and that ReactorGLES::React flushes on the same thread:

  • SIGSEGV inside the GL driver (libGLES_mali.so, libGLESv2_mtk.so, libGLESv2_adreno.so), called from ConfigureFBO → TextureGLES::SetAsFramebufferAttachment (blit_command_gles.cc:49), through GLProc<void(GLenum, GLenum, GLenum, GLuint, GLint, GLsizei)>, i.e. glFramebufferTexture2DMultisampleEXT. Fault addresses are small (0x3fa, 0x58, 0x1018), a near null dereference inside the driver.
  • SIGABRT from FML_CHECK(result) << "Must be able to encode GL commands without error." in the BlitPassGLES::EncodeCommands lambda (blit_pass_gles.cc:88), when Encode returned false. At that revision Encode returns false when ConfigureFBO returns nullopt (no GL handle for the texture, attachment failure, or incomplete framebuffer) or when the pixel format is unsupported. The VALIDATION_LOG reason is not available in a release build.

Frames from AndroidSurfaceGLImpeller::CreateSnapshotSurface, GPUSurfaceGLImpeller::GPUSurfaceGLImpeller and ContentContext::ContentContext (content_context.cc:600) appear in about 70% of these stacks, so the readback executes while a snapshot pbuffer surface is, or was just, current.

Caveat: arm64 NDK stacks in Crashlytics are stack scanned, so the frame order is approximate and some frames are noise. The frames listed here recur in dozens of independent events (126 of 136 decoded crashes).

A code sample that runs the same sequence is below. We have not been able to run it on an affected device, so it is the sequence, not a confirmed repro. The repro repository posted on #193428 exercises the same readback (toByteData on GLES, MediaTek + Mali) and may serve as a starting point.

Expected results

Image.toByteData() completes with the pixels, or completes with an error that Dart code can catch. The process does not die.

Actual results

The process is killed by SIGSEGV in the GL driver or by SIGABRT from FML_CHECK. Crashlytics splits this into four groups by blamed frame.

Over the last 7 days on our 3.47.3 build (6 Android apps combined):

Shape Users
SIGSEGV under ConfigureFBO (2 groups) 533
SIGABRT on FML_CHECK in BlitPassGLES::EncodeCommands (2 groups) 428

About 960 users a week. On our largest app it is about 70% of all crashed users on that build.

Devices (136 decoded crashes): TECNO KM7 (19), TECNO KM6 (15), TECNO KM7k (8), TECNO CL6k (6), Infinix X6880 (6), TECNO CM6 (5), TECNO KJ7 (5), TECNO KJ5 (5), TECNO CK6 (4), TECNO CLA5 (4), Samsung SM-G973U (3), Honor ABR-NX1 (3), TECNO KL6, Samsung SM-G960F, TECNO KG5j, itel A669W, Honor FNE-NX9, Xiaomi 25062RN2DA (2 each), and singles. GL driver in the stack: Mali 102, Adreno 17, MediaTek GLESv2 3. Android 10 to 17 (Android 15: 72, Android 14: 24). arm64 128, armeabi-v7a 8.

Most are MediaTek phones that 3.47 sends to OpenGL ES (#186405, #193428). The Honor (Adreno) and Samsung S10 (SM-G973U, Adreno 640) crashes show the GLES readback path itself is affected, not one vendor driver.

Timing: 25 of 136 crashes happened within 5 s of the app returning to the foreground. The others follow a route change (median 4.5 s after the last navigation breadcrumb).

History: until our previous build this sampler used Scene.toImage and hit #192365 on the same devices (the BlitCopyBufferToTextureCommandGLES upload flushed into freed memory by a later snapshot). Replacing the Scene with a Picture removed that crash and surfaced the two shapes above.

Related

What would fix it for us upstream

  1. A failed Encode inside a deferred blit op must not abort the process. BlitPassGLES::EncodeCommands should carry the failure to the command buffer completion, so Image.toByteData completes with an error. Our code already treats a failed readback as "no reading".
  2. ConfigureFBO should not hand the texture's GL name to glFramebufferTexture2DMultisampleEXT when the texture is not usable in the current context (a snapshot result read from another context, or a collected handle), or the snapshot result should be read back through its own render target.
  3. Does [impeller:gles] fixes early calls to flatten scenes #194506 change this path, since it reworks the GLES snapshot surface? If so, is a cherry-pick to 3.47 planned (requested on that PR)?

Workaround on the app side: skip the readback on OpenGL ES devices. The engine does not expose the backend, so the app has to replay android_context_dynamic_impeller.cc's selection on device facts.

Code sample

Code sample

The sequence our app runs. Not confirmed as a standalone repro on an affected device.

import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

void main() => runApp(const App());

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> with WidgetsBindingObserver {
  final GlobalKey _boundary = GlobalKey();
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _timer = Timer.periodic(const Duration(seconds: 2), (_) => _readStrip());
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      WidgetsBinding.instance.addPostFrameCallback((_) => _readStrip());
    }
  }

  Future<void> _readStrip() async {
    if (WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) return;
    final RenderRepaintBoundary? boundary =
        _boundary.currentContext?.findRenderObject() as RenderRepaintBoundary?;
    final OffsetLayer? layer = boundary?.layer as OffsetLayer?;
    if (boundary == null || layer == null) return;

    // Record the boundary's pictures into a Picture (no Scene), then read 16 columns back.
    final double pixelRatio = 16 / boundary.size.width;
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(recorder)..scale(pixelRatio);
    for (Layer? child = layer.firstChild; child != null; child = child.nextSibling) {
      if (child is PictureLayer && child.picture != null) canvas.drawPicture(child.picture!);
    }
    final ui.Picture picture = recorder.endRecording();
    try {
      final ui.Image image = await picture.toImage(16, (pixelRatio * 24).ceil());
      try {
        final ByteData? pixels = await image.toByteData(); // crashes here on Impeller GLES
        debugPrint('read ${pixels?.lengthInBytes} bytes');
      } finally {
        image.dispose();
      }
    } finally {
      picture.dispose();
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: RepaintBoundary(
          key: _boundary,
          child: Scaffold(
            appBar: AppBar(title: const Text('Readback')),
            body: ListView.builder(itemBuilder: (_, i) => ListTile(title: Text('Row $i'))),
          ),
        ),
      );
}

Screenshots or Video

No visual output, the process dies.

Logs

SIGSEGV, symbolicated (TECNO KM7k, Android 15, arm64, app build on Flutter 3.47.3)
Crashed: SIGSEGV, fault address 0x3fa

#00 libGLES_mali.so                 +0x70cc78
#02 libflutter.so  impeller::GLProc<void (GLenum, GLenum, GLenum, GLuint, GLint, GLsizei)>::operator()  (proc_table_gles.h:138)   [glFramebufferTexture2DMultisampleEXT]
#04 libGLES_mali.so                 +0x70e328
#06 libGLES_mali.so                 +0x7154f4
#08 libGLES_mali.so                 +0x70c8dc
#09 libGLES_mali.so                 +0x6f9534
#11 libGLES_mali.so                 +0x70c540
#13 libflutter.so  impeller::ConfigureFBO(impeller::ProcTableGLES const&, std::shared_ptr<impeller::Texture> const&, GLenum)  (blit_command_gles.cc:49)
#22 libflutter.so  impeller::BlitCopyTextureToBufferCommandGLES::Encode(impeller::ReactorGLES const&) const  (blit_command_gles.cc:305)
#36 libflutter.so  impeller::EncodeCommandsInReactor(...)  (blit_pass_gles.cc:67)  <- impeller::BlitPassGLES::EncodeCommands() const::$_0::operator()
#47 libflutter.so  impeller::ReactorGLES::FlushOps()  <- ReactorGLES::ReactOnce() (reactor_gles.cc:293)  <- ReactorGLES::React() (reactor_gles.cc:261)
#54 libflutter.so  impeller::ReactorGLES::AddOperation(std::function<void (impeller::ReactorGLES const&)>, bool)  (reactor_gles.cc:184)
#56 libflutter.so  impeller::BlitPassGLES::EncodeCommands() const  (blit_pass_gles.cc:85)
#51 libflutter.so  impeller::BlitPassGLES::OnCopyTextureToBufferCommand(...)  (blit_pass_gles.cc:121)
#53 libflutter.so  impeller::BlitPass::AddCopy(std::shared_ptr<impeller::Texture>, std::shared_ptr<impeller::DeviceBuffer>, ...)  (blit_pass.cc:113)
#59 libflutter.so  flutter::ImageEncodingImpeller::ConvertDlImageToSkImage(...)  (image_encoding_impeller.cc)  <- DoConvertImageToRasterImpeller(...)::$_1::operator() (image_encoding_impeller.cc:78)

Dart frames in the same stack (libapp.so):
    _Image.toByteData  (painting.dart:2225)
    _AndroidSystemOverlaysContrastState._readIcons  (system_overlays_contrast.dart:199)   [our sampler]
    _NativePicture.toImage.<anonymous closure>  (painting.dart:8516)
SIGABRT, symbolicated (Xiaomi 2203129G, Android 14, arm64, same build)
Crashed: SIGABRT

#00 libc.so                         abort
#02 libflutter.so  fml::LogMessage::~LogMessage()  (logging.cc)
#03 libflutter.so  impeller::BlitPassGLES::EncodeCommands() const::$_0::operator()(impeller::ReactorGLES const&)  (blit_pass_gles.cc:88)
                   FML_CHECK(result) << "Must be able to encode GL commands without error."
#13 libflutter.so  impeller::ReactorGLES::FlushOps()  <- ReactOnce() (reactor_gles.cc:293)  <- React() (reactor_gles.cc:261)
#21 libflutter.so  impeller::ReactorGLES::AddOperation(...)  (reactor_gles.cc:184)
#23 libflutter.so  impeller::BlitPassGLES::EncodeCommands() const  (blit_pass_gles.cc:85)
#17 libflutter.so  impeller::BlitPassGLES::OnCopyTextureToBufferCommand(...)  (blit_pass_gles.cc:121)
#19 libflutter.so  impeller::BlitPass::AddCopy(...)  (blit_pass.cc:113)
#26 libflutter.so  flutter::ImageEncodingImpeller::ConvertDlImageToSkImage(...)  <- DoConvertImageToRasterImpeller(...)::$_1::operator() (image_encoding_impeller.cc:78)
#35 libflutter.so  flutter::Picture::DoRasterizeToImage(...)  (picture.cc:242)
#54 libflutter.so  flutter::(anonymous namespace)::DoConvertImageToRasterImpellerWithRetry(...)  (image_encoding_impeller.cc:94)
#61 libflutter.so  flutter::(anonymous namespace)::EncodeImageAndInvokeDataCallback(...)

Dart frame in the same stack (libapp.so):
    _NativePicture.toImage.<anonymous closure>  (painting.dart:8516)

Symbolication: llvm-symbolizer --obj=libflutter.so --relative-address against flutter_infra_release/flutter/06a2e2a110089dff50fe635cffd2a61e1b24fbcd/android-arm64-release/symbols.zip (build id 1164bae6165f15f0ecdcba31ceb73ad2ea73038d).

Flutter Doctor output

Doctor output

Release builds come from CI, so this is the toolchain rather than a developer machine's flutter doctor -v:

Flutter 3.47.3 (stable), engine revision 06a2e2a110089dff50fe635cffd2a61e1b24fbcd
Dart 3.11 (bundled)
Android release build, arm64-v8a and armeabi-v7a, minSdk 26, targetSdk = flutter.targetSdkVersion
Impeller enabled by default, no renderer flags in AndroidManifest.xml (no EnableHcpp, no EnableSurfaceControl)
Crashlytics NDK 20.0.6

Activity

  1. suryaKommana2662 commented on Oct 27, 2026

    @suryaKommana2662
    Contributor

    @WessimBetclic Thanks for the crash report, reproduction sample, and production stack traces. The crash appears to occur in the Impeller GLES image readback path. Routing to team-engine for further investigation.
    Related issues : #192365 and #190640

  2. added
    platform-androidAndroid applications specifically
    engineflutter/engine related. See also e: labels.
    e: impellerImpeller rendering backend issues and features requests
    and removed
    in triagePresently being triaged by the triage team
    on Oct 27, 2026
  3. WessimBetclic commented on Oct 27, 2026

    @WessimBetclic
    Author

    thanks for your answer @suryaKommana2662, I submitted a draft PR waiting for CLA handling by my company's manager

  4. gaaclarke commented on Nov 2, 2026

    @gaaclarke
    Member

    no local reproduction yet

    What does the linked code do if there is no local reproduction? This is going to be hard to make progress on without some sort of reproduction. We do have that fix that landed recently for when there is no surface yet. That could be related, but it's impossible to tell without a reproduction.

  5. added
    needs repro infoAutomated crash report whose cause isn't yet known
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    P2Important issues not at the top of the work list
    on Nov 2, 2026
  6. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Nov 3, 2026
  7. WessimBetclic commented on Nov 3, 2026

    @WessimBetclic
    Author

    @gaaclarke the PR doesn't fix the crash, it only stops the process from aborting when the driver refuses the framebuffer, the test triggers that through the mock GL. I still can't reproduce it on a device.

    I think I found the actual cause by reading the stacks against the code. Since #174668 the readback binds GL_FRAMEBUFFER, so ComputeTypeForBinding no longer attaches the MSAA snapshot texture single sampled the way #163345 did for GL_READ_FRAMEBUFFER, and we end up in glFramebufferTexture2DMultisampleEXT just to read pixels. That's the frame in our SIGSEGV stacks. 3.47 exposed it by moving MediaTek phones to GLES.

    #195887 keeps GL_FRAMEBUFFER but attaches single sampled for the readback, with a mock test that fails on master. The no-surface fix doesn't touch that path, so I don't think it's related.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work liste: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.needs repro infoAutomated crash report whose cause isn't yet knownplatform-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