Repository navigation
Description
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:
- Replay the layer of a
RepaintBoundaryinto aui.Picture(aPictureRecorderpluscanvas.drawPictureof the layer tree's pictures, noScene). picture.toImage(w, h)for a 16 pixel wide strip of the status bar area, so a 16x1 or 16x2 image.image.toByteData()(rawRgba), thenimage.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 fromConfigureFBO→TextureGLES::SetAsFramebufferAttachment(blit_command_gles.cc:49), throughGLProc<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 theBlitPassGLES::EncodeCommandslambda (blit_pass_gles.cc:88), whenEncodereturned false. At that revisionEncodereturns false whenConfigureFBOreturnsnullopt(no GL handle for the texture, attachment failure, or incomplete framebuffer) or when the pixel format is unsupported. TheVALIDATION_LOGreason 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
- [Impeller][Android] 3.47 regression: MediaTek/Mali phones fall back to OpenGLES and freeze (ANR) —
1.iostuck inglReadPixels(libGLES_mali.so osup_sync_object_wait), main thread stuck in `nativeSurfaceDestroyed #193428: same 3.47 OpenGL ES fallback on MediaTek, readback freezes inglReadPixelson the IO thread. Ours crashes instead of freezing, on the raster side of aPicture.toImageresult. - [Impeller] GLES: SIGSEGV use-after-free in DeviceBufferGLES::OnGetContents — deferred ReactorGLES ops outlive their DeviceBuffer and are flushed by a later Picture.toImage snapshot #192365, fixed by [impeller:gles] fixes early calls to flatten scenes #194506 on master: the previous shape of this crash in our app.
- [Impeller][Android][GLES] FragmentProgram causes SIGABRT on second app launch — "Must be able to encode GL commands without error" #186186: same
FML_CHECKsite, reached fromFragmentProgram. - [Impeller] GLES: copying a wrapped texture to a buffer reads the default framebuffer #194150 / [Impeller] Read back wrapped GL textures instead of the default framebuffer #194160:
ConfigureFBObinding the default framebuffer for wrapped textures.
What would fix it for us upstream
- A failed
Encodeinside a deferred blit op must not abort the process.BlitPassGLES::EncodeCommandsshould carry the failure to the command buffer completion, soImage.toByteDatacompletes with an error. Our code already treats a failed readback as "no reading". ConfigureFBOshould not hand the texture's GL name toglFramebufferTexture2DMultisampleEXTwhen 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.- 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
suryaKommana2662 commentedon Oct 27, 2026
@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-enginefor further investigation.Related issues : #192365 and #190640
WessimBetclic commentedon Oct 27, 2026
thanks for your answer @suryaKommana2662, I submitted a draft PR waiting for CLA handling by my company's manager
gaaclarke commentedon Nov 2, 2026
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.
[Impeller] Log GLES pass encoding failures instead of aborting
WessimBetclic commentedon Nov 3, 2026
@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.