Repository navigation
Description
Steps to reproduce
Field conditions (no minimal repro yet — analysis below is from symbolicated production crashes plus reading the engine source at the shipped revision):
- Production Flutter 3.47.2 app with the Sentry Flutter SDK capturing UI snapshots via
RepaintBoundary.toImage()/Picture.toImage()at ~1 fps (session replay) and on errors (screenshot attachment). - Run on an Android device where Impeller selects the OpenGLES backend (Vulkan-blocklisted low-end hardware). Crashing devices observed: TCL 20 SE (Adreno 610, Android 12), Samsung Galaxy A13 5G (Mediatek MT6833/Mali, Android 14), Google Play pre-launch-report emulators.
- Have a snapshot race a moment with no onscreen surface (backgrounding / surface teardown). Two of our production crashes on different devices occurred 21 seconds apart, consistent with a push-driven background wake triggering capture while the surface was gone.
Suggested repro sketch: on a GLES-backend device or emulator, loop RepaintBoundary.toImage() while cycling the activity background↔foreground to force surface teardown mid-capture.
Expected results
Offscreen snapshots either succeed or fail gracefully. Specifically: deferred GLES reactor operations keep the DeviceBuffers they reference alive (or are invalidated when the owner is destroyed), and a failed CreateSnapshotSurface aborts the snapshot instead of encoding without a current EGL context.
Actual results
Native SIGSEGV on the raster thread at impeller::DeviceBufferGLES::OnGetContents (device_buffer_gles.cc:34). 5+ crashes across 3 device models and 4 app releases, always this exact stack, only on Impeller-GLES devices. iOS (Metal) and Vulkan-backend Android are unaffected.
Analysis
All file/line references are engine revision a804b261645ef8c13eb3d5c44a5c2fb0340c5539 (Flutter 3.47.2 stable). The consumer side of the defect is proven by the stack; the producer side (which teardown queued the stale ops) is inferred — several deferral paths share the same defect.
-
No-surface entry, no early return.
shell/common/snapshot_controller_impeller.cc:78-91— whendelegate.GetSurface()is null (surface torn down),CreateSnapshotSurface()is called and there is no early-return on failure:DoMakeRasterSnapshotruns even if the pbuffer surface is null / not current, so the Impeller encode can execute with no EGL context current. -
Ops defer silently.
ReactorGLES::AddOperation(impeller/renderer/backend/gles/reactor_gles.cc:173-186) appends ops to a per-thread queue; reacting is skipped whenCanReactOnCurrentThread()is false (toggled by EGLkDidMakeCurrent/kWillClearCurrent,shell/platform/android/android_context_gl_impeller.cc:194-199). The code comments "it is not an error if this isn't possible" — the ops just wait. -
Queued ops hold raw, non-owning buffer pointers.
impeller/core/buffer_view.h:36-41—BufferViewholds a non-ownedconst DeviceBuffer*("Steps should be taken to make sure this lives for the duration of the BufferView's life"). The deferred blit op keeps theBlitPassGLESalive (shared_from_this,blit_pass_gles.cc:84) but not the DeviceBuffer it reads from. When the owningContentContext/HostBufferis destroyed — ad-hoc snapshot contexts die at the end of each snapshot; the onscreenAiksContextdies on every surface teardown — the queued op's pointer dangles. -
Fatal flush on the next snapshot.
AndroidSurfaceGLImpeller::CreateSnapshotSurface(shell/platform/android/android_surface_gl_impeller.cc:149-167) makes a fresh offscreen pbuffer current and constructs a newGPUSurfaceGLImpeller→AiksContext→ContentContext, whose constructor uploads a 1×1empty_texture_viaBlitPass::EncodeCommands(content_context.cc:600). NowReact()succeeds andFlushOps()(reactor_gles.cc:366-388) drains the per-thread FIFO stale backlog first →EncodeCommandsInReactor(blit_pass_gles.cc:67) →BlitCopyBufferToTextureCommandGLES::Encode(blit_command_gles.cc:208-209):const GLvoid* tex_data = source.GetBuffer()->OnGetContents() + source.GetRange().offset;
→
device_buffer_gles.cc:30-35:uint8_t* DeviceBufferGLES::OnGetContents() const { if (!reactor_) { return nullptr; } return backing_store_->GetBuffer(); // line 34 — SIGSEGV }
On a freed
DeviceBufferGLESthereactor_slot reads as non-null garbage and line 34 dereferences a garbagebacking_store_— a use-after-free, at exactly the reported frame. One captured event even showsstd::unique_ptr::operator->inlined at theCreateSnapshotSurfaceframe.
Ruled out: a failed-allocation/null BufferView — empty views have range length 0 and are rejected by BlitPass::AddCopy (blit_pass.cc:117-147), so the crashing pointer must be a once-valid, now-freed buffer.
Why GLES only: the Vulkan backend lifetime-tracks command-referenced resources (TrackedObjectsVK); the GLES reactor uniquely combines deferred per-thread ops with raw pointers.
Related: #191168 (Vulkan flavor of GPU-work-racing-surface-teardown), #192097 (same toImage snapshot path, memory leak), getsentry/sentry-dart#3923 (Sentry SDK maintainers concluding this class of crash needs an engine fix).
Logs
Symbolicated crash stack (production, Sentry)
SIGSEGV: Segfault (raster thread)
flutter::Picture::DoRasterizeToImage (picture.cc:265)
flutter::Rasterizer::MakeImpellerSnapshot (rasterizer.cc:459)
flutter::SnapshotControllerImpeller::MakeImpellerSnapshot (snapshot_controller_impeller.cc:137 → 156)
flutter::(anonymous namespace)::DoMakeRasterSnapshot (snapshot_controller_impeller.cc:84)
flutter::AndroidSurfaceGLImpeller::CreateSnapshotSurface (android_surface_gl_impeller.cc:163) // unique_ptr::operator-> inlined here in one event
flutter::GPUSurfaceGLImpeller::GPUSurfaceGLImpeller (gpu_surface_gl_impeller.cc:28)
impeller::AiksContext::AiksContext (aiks_context.cc:21)
impeller::ContentContext::ContentContext (content_context.cc:600) // 1×1 empty_texture_ upload
impeller::BlitPassGLES::EncodeCommands (blit_pass_gles.cc:85)
impeller::ReactorGLES::AddOperation (reactor_gles.cc:184)
impeller::ReactorGLES::React (reactor_gles.cc:261)
impeller::ReactorGLES::ReactOnce (reactor_gles.cc:293)
impeller::ReactorGLES::FlushOps (reactor_gles.cc:385)
impeller::EncodeCommandsInReactor (blit_pass_gles.cc:67)
impeller::BlitCopyBufferToTextureCommandGLES::Encode (blit_command_gles.cc:209)
impeller::DeviceBufferGLES::OnGetContents (device_buffer_gles.cc:34) // ← SIGSEGV
Flutter Doctor output
flutter doctor -v
[✓] Flutter (Channel stable, 3.47.2, on macOS 26.6.2 25G83 darwin-arm64, locale en-IE)
• Flutter version 3.47.2 on channel stable at /opt/homebrew/share/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision d3b14c8769 (2026-08-26)
• Engine revision a804b26164
• Dart version 3.13.2
• DevTools version 2.60.0
Crashes are from production Android release builds (Play Store installs), not the development machine.
Activity
mbcorona commentedon Sep 7, 2026
HI @bjrochem72 , thanks for the detailed report and for sharing the logs. Routing to the engine team for further investigation.
walley892 commentedon Sep 15, 2026
Hi @bjrochem72, getsentry/sentry-dart#3935 claims to fix this issue in Sentry. Could you update and see if this is the case?
bjrochem72 commentedon Sep 15, 2026
Thanks @walley892 — I'll give it a try. Plan is to bump to
sentry_flutter9.30.0, re-enable Android screenshots (I'd turned them off as a workaround), and watch my Sentry crash counts to see if it clears. Will report back.vib3App commentedon Oct 8, 2026
We hit the same crash (same stack,
BlitCopyBufferToTextureCommandGLES::Encode→DeviceBufferGLES::OnGetContents, fault addr 0x0) on Flutter 3.47.6 (engine692136cb6582dbfc5af3fb33c2515a069f2f66d0), and have a deterministic minimal reproduction plus the concrete producer of the stale upload.Minimal reproduction
Android emulator API 34 (Impeller selects OpenGL ES),
flutter build apk --profile:runAppScene.toImage(above)Picture.toImage(above)Each crash is preceded on the raster thread by
E libEGL: call to OpenGL ES API with no current context (logged once per thread). A fuller harness gave the same split:Scene+ImageFilter.shader6/6,Scene.toImageSync4/4,Picturewith the same filter on asaveLayer0/11. In a production app the same crash happens on a launch into a process Android kept alive (Back, then open the app again; or a process a service kept up), becausemain()then reaches the snapshot beforeSurfaceView.surfaceCreated. Also seen on a OnePlus 8 Pro (Adreno 650 → Vulkan rejected → GLES), Android 13, release build.The producer
The stale op comes from layer-tree flattening, which builds a throwaway
AiksContextbefore any EGL context is current:Picture::DoRasterizeToImage,layer_treebranch (lib/ui/painting/picture.cc:252-262), callssnapshot_delegate->GetAiksContext()to flatten the scene beforeMakeImpellerSnapshot. The sync path does the same:DlDeferredImageGPUImpeller::ImageWrapper::SnapshotDisplayList(lib/ui/painting/display_list_deferred_image_gpu_impeller.cc:175-183).Rasterizer::GetAiksContext()(shell/common/rasterizer.h:712-722) constructs a newAiksContexton the raster thread while nothing is current — hence the libEGL line.ContentContextconstructor emplaces the 4-byteempty_texture_upload into its ownHostBuffer(non-owningBufferView,host_buffer.cc:196-204) and encodes the blit;ReactorGLES::React()cannot run (no reactions allowed on that thread), so the op stays queued.AiksContextis destroyed at the end of theif (layer_tree)block, freeing theHostBuffer'sDeviceBufferGLES.DoMakeRasterSnapshot→AndroidSurfaceGLImpeller::CreateSnapshotSurfacemakes a pbuffer current and builds anotherGPUSurfaceGLImpeller/AiksContext; its ownempty_texture_upload callsReact(), andFlushOps()drains the stale op first →OnGetContents()on the freed buffer.Picture.toImagepasses no layer tree, so step 1 never happens:DoMakeRasterSnapshot(shell/common/snapshot_controller_impeller.cc:70-92) makes the pbuffer current before anyAiksContextis built, and every upload flushes while current.This also explains the background reports above: on Android the app hears
AppLifecycleState.hiddenaftersurfaceDestroyed(we measured 33 ms after), so aRepaintBoundary.toImage(Sync)taken on hidden/paused goes through the same flattening with no surface.Workaround we shipped
For snapshots that can run before the first frame or while the app is in the background on Android, use a
Picture(canvas.saveLayer(bounds, Paint()..imageFilter = filter)gives the same filtered layer anImageFilterLayerflattens to) instead of aScene/RepaintBoundary/OffsetLayersnapshot.Fix directions, for what they're worth: make the snapshot context current before flattening (and reuse that
AiksContextfor the rasterization), have deferred GLES uploads own their source buffer, and skip building anAiksContextwhen the snapshot surface could not be made current (the no-early-return case in the original report can still recreate this).Inline video: no screen snapshot from the background (Impeller GLES S…
[impeller:gles] fixes early calls to flatten scenes (flutter#194506)
[impeller:gles] fixes early calls to flatten scenes (flutter#194506)
github-actions commentedon Oct 31, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.