Skip to content

Session Replay: in-flight capture outlives Vulkan surface teardown → native SIGABRT in ~TextureVK (Android/Impeller) #3923

Open

Description

@rkuzmin

Platform

Flutter Mobile Android

SDK versions

  • sentry_flutter / sentry / sentry_dio: 9.24.0
  • Flutter: 3.41.7 stable, release build (Impeller / Vulkan backend)
  • Device: Samsung Galaxy A15 5G (SM-A156E), MediaTek MT6835 / Mali-G57, Android 15

Configuration

options.replay.sessionSampleRate = 0.03;
options.replay.onErrorSampleRate = 1.0;
// app wrapped in SentryWidget(child: app)

What happened

A production user hit a native SIGABRT 3.000 s after MainActivity was destroyed. The crashing stack is an Impeller Vulkan texture teardown, reached from a picture.cc completion lambda being destroyed during a message-loop task flush:

android::Looper::pollOnce
fml::MessageLoopAndroid::OnEventFired          (message_loop_android.cc:91)
fml::MessageLoopImpl::RunExpiredTasksNow       (message_loop_impl.cc:139)
fml::MessageLoopImpl::FlushTasks               (message_loop_impl.cc:122)
std::__fl::function<T>::operator=              (function.h:968)
std::__fl::__function::__func<T>::destroy_deallocate (function.h:313)
const::lambda::~lambda                         (picture.cc:244)
sk_sp<T>::~sk_sp                               (SkRefCnt.h:256)
impeller::DlImageImpeller::~DlImageImpeller    (dl_image_impeller.cc:84)
impeller::TextureVK::~TextureVK                (texture_vk.cc:25)
impeller::TextureSourceVK::~TextureSourceVK    (texture_source_vk.cc:11)
impeller::Attachment::~Attachment              (formats.h:660)
...
abort

Lifecycle breadcrumbs immediately before it:

16:09:26.018  MainActivity paused
16:09:26.592  LOW_MEMORY (TRIM_MEMORY level 40)
16:09:26.601  MainActivity stopped
16:09:26.614  MainActivity destroyed
16:09:29.614  SIGABRT

Why I think this is the replay recorder

I can't attach a reproduction, so this is attribution by elimination — but the elimination is fairly tight:

  1. Our app contains exactly one RenderRepaintBoundary.toImage() call site, on a share-a-post screen. The affected user was a signed-out guest whose entire six-minute session was an onboarding flow; breadcrumbs show they never authenticated and never opened that screen. That path provably did not run.

  2. Nothing else in our code calls toImage / toImageSync / Picture.toImage.

  3. That leaves the replay recorder as the only in-process producer, and it matches the stack exactly — lib/src/screenshot/recorder.dart:

    • L84 final futureImage = renderObject.toImage(pixelRatio: capture.pixelRatio);
    • L223 finalImage = await picture.toImage(width, height);

    Picture.toImage is Picture::RasterizeToImage, and the completion lambda in picture.cc is what owns the sk_sp<DlImage> whose destruction runs ~DlImageImpeller → ~TextureVK in the stack above.

Suspected race

ScheduledScreenshotRecorder.pause() / stop() (lib/src/replay/scheduled_recorder.dart L99–L120) set _status and stop the Scheduler, but they don't cancel or await a capture that is already in flight.

A capture that started just before the lifecycle transition still runs to completion on its own async chain: await futureImage → await image.toByteData() → canvas.drawImage → picture.toImage(width, height) → Screenshot(finalImage). Only then does _onImageCaptured observe _status != running and drop the frame.

The Dart-side Screenshot.dispose() does run in the finally, but releasing the image only posts the GPU-side texture release to the engine's message loop. By the time that task is flushed, the Activity and its Vulkan surface are gone, and destroying TextureVK against a dead context aborts inside the Mali driver. The TRIM_MEMORY 40 immediately beforehand plausibly accelerates the surface teardown and widens the window.

Suggested direction

On pause() / stop(), either await in-flight captures before returning (so the textures are released while the context is still alive), or refuse to start a capture once the app is no longer resumed. Dropping the finished frame at the end is enough for correctness of the replay itself, but not for the GPU resource it already allocated.

Caveats

Single event, no reproduction, and the native frames come from a release build. I'm reporting it because the code path lines up precisely and the failure mode (deferred GPU resource release racing surface teardown) looks structural rather than device-specific, even if this particular driver is what turns it into an abort.

Activity

  1. linear-code commented on Jul 26, 2026

    @linear-code
  2. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jul 26, 2026
  3. lucas-zimerman commented on Jul 29, 2026

    @lucas-zimerman
    Collaborator

    Hi and thanks for opening this issue! We will investigate it and give you an update as soon as we get some news

  4. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jul 29, 2026
  5. buenaflor commented on Aug 20, 2026

    @buenaflor
    Contributor

    @lucas-zimerman it's possible that it is a Flutter issue as well: flutter/flutter#191168

    We've merged a mitigation to minimize the window but it's not a full fix

    For a full fix we should wait for flutter/flutter#192365

  6. github-actions commented on Sep 10, 2026

    @github-actions
    Contributor

    A PR addressing this issue has just been released 🚀

    This issue was referenced by PR #3935, which was included in the 9.30.0 release.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions