Skip to content

[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

Closed

Description

@bjrochem72

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):

  1. 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).
  2. 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.
  3. 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.

  1. No-surface entry, no early return. shell/common/snapshot_controller_impeller.cc:78-91 — when delegate.GetSurface() is null (surface torn down), CreateSnapshotSurface() is called and there is no early-return on failure: DoMakeRasterSnapshot runs even if the pbuffer surface is null / not current, so the Impeller encode can execute with no EGL context current.

  2. Ops defer silently. ReactorGLES::AddOperation (impeller/renderer/backend/gles/reactor_gles.cc:173-186) appends ops to a per-thread queue; reacting is skipped when CanReactOnCurrentThread() is false (toggled by EGL kDidMakeCurrent / 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.

  3. Queued ops hold raw, non-owning buffer pointers. impeller/core/buffer_view.h:36-41 — BufferView holds a non-owned const DeviceBuffer* ("Steps should be taken to make sure this lives for the duration of the BufferView's life"). The deferred blit op keeps the BlitPassGLES alive (shared_from_this, blit_pass_gles.cc:84) but not the DeviceBuffer it reads from. When the owning ContentContext/HostBuffer is destroyed — ad-hoc snapshot contexts die at the end of each snapshot; the onscreen AiksContext dies on every surface teardown — the queued op's pointer dangles.

  4. 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 new GPUSurfaceGLImpeller → AiksContext → ContentContext, whose constructor uploads a 1×1 empty_texture_ via BlitPass::EncodeCommands (content_context.cc:600). Now React() succeeds and FlushOps() (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 DeviceBufferGLES the reactor_ slot reads as non-null garbage and line 34 dereferences a garbage backing_store_ — a use-after-free, at exactly the reported frame. One captured event even shows std::unique_ptr::operator-> inlined at the CreateSnapshotSurface frame.

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

  1. added
    engineflutter/engine related. See also e: labels.
    in triagePresently being triaged by the triage team
    e: impellerImpeller rendering backend issues and features requests
    c: crashStack traces logged to the console
    platform-androidAndroid applications specifically
    and removed
    in triagePresently being triaged by the triage team
    on Sep 7, 2026
  2. mbcorona commented on Sep 7, 2026

    @mbcorona
    Member

    HI @bjrochem72 , thanks for the detailed report and for sharing the logs. Routing to the engine team for further investigation.

  3. added
    P1High-priority issues at the top of the work list
    on Sep 14, 2026
  4. walley892 commented on Sep 15, 2026

    @walley892
    Contributor

    Hi @bjrochem72, getsentry/sentry-dart#3935 claims to fix this issue in Sentry. Could you update and see if this is the case?

  5. bjrochem72 commented on Sep 15, 2026

    @bjrochem72
    Author

    Thanks @walley892 — I'll give it a try. Plan is to bump to sentry_flutter 9.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.

  6. vib3App commented on Oct 8, 2026

    @vib3App

    We hit the same crash (same stack, BlitCopyBufferToTextureCommandGLES::Encode → DeviceBufferGLES::OnGetContents, fault addr 0x0) on Flutter 3.47.6 (engine 692136cb6582dbfc5af3fb33c2515a069f2f66d0), 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:

    import 'dart:ui' as ui;
    
    import 'package:flutter/material.dart';
    
    // adb shell am start -n <pkg>/.MainActivity --es route /scene    (or /picture)
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      final mode = ui.PlatformDispatcher.instance.defaultRouteName;
    
      // Before runApp: on Android the FlutterView has no window surface yet.
      final recorder = ui.PictureRecorder();
      ui.Canvas(recorder).drawRect(
          const Rect.fromLTWH(0, 0, 8, 8), Paint()..color = const Color(0xFF2ADEC3));
      final picture = recorder.endRecording();
      final ui.Image image;
      if (mode == '/picture') {
        image = await picture.toImage(8, 8); // fine
      } else {
        final scene = (ui.SceneBuilder()..addPicture(Offset.zero, picture)).build();
        image = await scene.toImage(8, 8); // SIGSEGV on Impeller OpenGL ES
      }
      debugPrint('REPRO done mode=$mode ${image.width}x${image.height}');
    
      runApp(const MaterialApp(home: Scaffold(body: Center(child: Text('ok')))));
    }
    Snapshot before runApp Launches SIGSEGV
    Scene.toImage (above) 4 4 (cold starts included)
    Picture.toImage (above) 4 0

    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.shader 6/6, Scene.toImageSync 4/4, Picture with the same filter on a saveLayer 0/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), because main() then reaches the snapshot before SurfaceView.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 AiksContext before any EGL context is current:

    1. Picture::DoRasterizeToImage, layer_tree branch (lib/ui/painting/picture.cc:252-262), calls snapshot_delegate->GetAiksContext() to flatten the scene before MakeImpellerSnapshot. The sync path does the same: DlDeferredImageGPUImpeller::ImageWrapper::SnapshotDisplayList (lib/ui/painting/display_list_deferred_image_gpu_impeller.cc:175-183).
    2. With no surface, Rasterizer::GetAiksContext() (shell/common/rasterizer.h:712-722) constructs a new AiksContext on the raster thread while nothing is current — hence the libEGL line.
    3. Its ContentContext constructor emplaces the 4-byte empty_texture_ upload into its own HostBuffer (non-owning BufferView, host_buffer.cc:196-204) and encodes the blit; ReactorGLES::React() cannot run (no reactions allowed on that thread), so the op stays queued.
    4. That temporary AiksContext is destroyed at the end of the if (layer_tree) block, freeing the HostBuffer's DeviceBufferGLES.
    5. DoMakeRasterSnapshot → AndroidSurfaceGLImpeller::CreateSnapshotSurface makes a pbuffer current and builds another GPUSurfaceGLImpeller / AiksContext; its own empty_texture_ upload calls React(), and FlushOps() drains the stale op first → OnGetContents() on the freed buffer.

    Picture.toImage passes no layer tree, so step 1 never happens: DoMakeRasterSnapshot (shell/common/snapshot_controller_impeller.cc:70-92) makes the pbuffer current before any AiksContext is built, and every upload flushes while current.

    This also explains the background reports above: on Android the app hears AppLifecycleState.hidden after surfaceDestroyed (we measured 33 ms after), so a RepaintBoundary.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 an ImageFilterLayer flattens to) instead of a Scene / RepaintBoundary / OffsetLayer snapshot.

    Fix directions, for what they're worth: make the snapshot context current before flattening (and reuse that AiksContext for the rasterization), have deferred GLES uploads own their source buffer, and skip building an AiksContext when the snapshot surface could not be made current (the no-early-return case in the original report can still recreate this).

  7. github-actions commented on Oct 31, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  8. locked as resolved and limited conversation to collaborators on Oct 31, 2026
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

    P1High-priority issues at the top of the work listc: crashStack traces logged to the consolee: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.found in release: 3.47Found to occur in 3.47platform-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