Skip to content

[iOS][Skia] RenderRepaintBoundary.toImage() of an NV12 FlutterTexture dereferences a null GrDirectContext (Flutter 3.24.5) #189555

Closed

Description

@ZGaopeng

Steps to reproduce

Reproduced on Flutter 3.24.5 in iOS release mode on a physical device:

  1. Disable Impeller in Info.plist:

    <key>FLTEnableImpeller</key>
    <false/>
  2. Render four live remote videos with AgoraVideoView and
    useFlutterTexture: true. The texture provider returns NV12
    CVPixelBuffers.

  3. Place the video textures under a RepaintBoundary.

  4. Call RenderRepaintBoundary.toImage() when the application enters
    AppLifecycleState.inactive.

  5. After all remote videos are rendering, rapidly switch to another foreground
    application and back. The successful run used approximately 0.5 seconds
    outside the application and 0.7 seconds inside it.

Build command:

flutter build ios --release \
  --dart-define=JOIN_MULTIPLE_CHANNEL_REPRO=true \
  --dart-define=SENTRY_DSN=<YOUR_TEST_SENTRY_DSN>

The target report was produced by a direct toImage() call. The failing
lifecycle path did not call SentryFlutter.captureScreenshot(),
Sentry.captureMessage(), or another Sentry capture API. sentry_flutter
remained initialized globally in this build.

The final lifecycle log was:

14:01:31 lifecycle=AppLifecycleState.inactive
14:01:31 begin toImage reason=lifecycle-inactive-toImage#0, remote0=4
14:01:31 completed toImage reason=lifecycle-inactive-toImage#0
14:01:33 lifecycle=AppLifecycleState.inactive
14:01:33 begin toImage reason=lifecycle-inactive-toImage#0, remote0=4
<no completion; process terminated>

Expected results

toImage()should complete, reject the snapshot, or fail gracefully. Flutter should not terminate the process with a nativeSIGSEGV`.

Actual results

Crash

EXC_BAD_ACCESS / SIGSEGV
KERN_INVALID_ADDRESS at 0x48
procRole: Foreground
thread: io.flutter.1.raster
Flutter.framework UUID: 4C4C4450-5555-3144-A1A5-C934648E82E4
first Flutter image offset: 0x7006a0

Symbolicated stack:

SkYUVAInfo::NumPlanes
SkYUVAInfo::numPlanes
SkImages::TextureFromYUVATextures
FlutterDarwinExternalTextureSkImageWrapper wrapYUVATexture
FlutterDarwinExternalTextureMetal wrapNV12ExternalPixelBuffer
FlutterDarwinExternalTextureMetal wrapExternalPixelBuffer
FlutterDarwinExternalTextureMetal onNeedsUpdatedTexture
FlutterDarwinExternalTextureMetal paintContext
TextureLayer::Paint
LayerTree::Flatten
Picture::DoRasterizeToImage

Null-context evidence

The faulting instructions are:

Flutter[0x70069c] <+900>: ldr x19, [sp, #0x8]
Flutter[0x7006a0] <+904>: ldr x23, [x19, #0x48]

The crash report records:

x19 = 0x0
FAR = 0x48
ESR = (Data Abort) byte read Translation fault

LLDB's DWARF lookup for this exact Flutter binary maps [sp + 0x8] to:

name = "context", type = "GrRecordingContext *"
location = DW_OP_breg31 WSP+8
decl = SkImage_GaneshYUVA.cpp:287

The wrapper argument is:

name = "grContext", type = "GrDirectContext *"
decl = FlutterDarwinExternalTextureMetal.mm:295

At Engine revision
a18df97ca57a249df5d8d68cd0820600223ce262, the wrapper calls Skia without a
runtime null check:

return SkImages::TextureFromYUVATextures(
    grContext, yuvaBackendTextures, nullptr, nullptr, nullptr);

This evidence identifies a null rendering context at the 0x48 access. The
inlined SkYUVAInfo::NumPlanes symbol does not by itself prove that the input
pixel buffer had an invalid plane count.

Control results

Variant Result
Skia; direct toImage() on lifecycle change; no lifecycle Sentry capture calls Target SIGSEGV on the second observed inactive transition
Skia; capture only while resumed No target crash in two independent runs of 20 and 50 rapid cycles
Skia; Sentry screenshot, direct toImage(), and event capture on inactive and resumed Three matching crashes in five cycles

NV12 observations

In the earlier inactive-plus-Sentry variant, paired producer and texture-copy
samples reported:

requestedFormat=12
vf.type=12
pixelFormat=420f
planes=2
badNV12=NO

Those 420f samples had matching anonymized buffer tokens, dimensions, pixel
formats, and plane counts. The direct-only process preserved the following
copy-boundary samples but no paired receive sample:

pixelFormat=420v
planes=2
badNV12=NO

No [AGORA_REPRO_BAD_NV12] marker was observed before the target crashes.

These logs sample frames; they do not prove that every frame or its lifetime
was valid. They do show that no malformed one-plane NV12 buffer was observed in
the captured evidence.

Current Engine relevance

This exact crash has been reproduced only on Flutter 3.24.5 with the iOS Skia
backend. Current official iOS builds force Slimpeller, disable Ganesh, and reject
the Skia backend, so this report does not claim that current iOS is affected:

The shared non-Slimpeller Darwin code still forwards context.gr_context to
Skia without a runtime null check. Current macOS source states that Skia remains
the default, and the Metal embedder external-texture path forwards the same
nullable context into the Skia wrappers:

No current macOS crash is claimed here. This report asks whether the shared
non-Slimpeller Darwin external-texture path should reject a null
GrDirectContext before calling Skia and add regression coverage that verifies
the frame is skipped instead of terminating the process.

This report does not request a Flutter 3.24.x hotfix.

Environment

Flutter: 3.24.5 stable
Framework revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
Engine revision: a18df97ca57a249df5d8d68cd0820600223ce262
Dart: 3.5.4
Build mode: release

Host: macOS 15.3.1 (24D70)
Xcode: 16.4 (16F6)
CocoaPods: 1.16.2
Device: iPhone13,2
iOS: 17.5.1 (21F90)

Agora Flutter SDK: 6.5.2-sp.452202
Agora iOS SDK: 4.5.2.202.2
Agora Iris iOS: 4.5.2.202-build.1
sentry_flutter: 9.20.0

Code sample

Code sample
final GlobalKey captureBoundaryKey = GlobalKey();

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  // The target crash occurred in inactive. The successful diagnostic harness
  // also captured on resumed as a control path.
  if (state == AppLifecycleState.inactive ||
      state == AppLifecycleState.resumed) {
    unawaited(capture());
  }
}

Future<void> capture() async {
  final context = captureBoundaryKey.currentContext;
  final boundary = context?.findRenderObject();
  if (boundary is! RenderRepaintBoundary) {
    return;
  }

  final image = await boundary.toImage(
    pixelRatio: View.of(context!).devicePixelRatio,
  );
  image.dispose();
}

@override
Widget build(BuildContext context) {
  return RepaintBoundary(
    key: captureBoundaryKey,
    child: AgoraVideoView(
      controller: VideoViewController.remote(
        rtcEngine: engine,
        canvas: VideoCanvas(uid: remoteUid),
        connection: connection,
        useFlutterTexture: true,
      ),
    ),
  );
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[direct-toimage-lifecycle-redacted.log](https://github.com/user-attachments/files/30072928/direct-toimage-lifecycle-redacted.log)
[flutter-3.24.5-symbolication-and-dwarf.txt](https://github.com/user-attachments/files/30072930/flutter-3.24.5-symbolication-and-dwarf.txt)
[flutter-doctor-v-redacted.txt](https://github.com/user-attachments/files/30072925/flutter-doctor-v-redacted.txt)
[nv12-boundary-trace-redacted.log](https://github.com/user-attachments/files/30072926/nv12-boundary-trace-redacted.log)
[repro-source-redacted.zip](https://github.com/user-attachments/files/30072929/repro-source-redacted.zip)
[Runner-direct-toimage-redacted.zip](https://github.com/user-attachments/files/30072927/Runner-direct-toimage-redacted.zip)

Flutter Doctor output

Doctor output
[OK] Flutter (Channel stable, 3.24.5, on macOS 15.3.1 24D70 darwin-arm64, locale zh-Hans-CN)
    - Flutter version 3.24.5 on channel stable at <REDACTED_FLUTTER_SDK_PATH>
    - Upstream repository https://github.com/flutter/flutter.git
    - Framework revision dec2ee5c1f, 2024-11-13 11:13:06 -0800
    - Engine revision a18df97ca5
    - Dart version 3.5.4
    - DevTools version 2.37.3

[OK] Android toolchain (Android SDK version 36.0.0)
    - Platform android-36, build-tools 36.0.0
    - Java version OpenJDK Runtime Environment 21.0.6
    - All Android licenses accepted

[OK] Xcode - develop for iOS and macOS (Xcode 16.4)
    - Xcode build 16F6
    - CocoaPods version 1.16.2

[OK] Chrome - develop for the web

[OK] Android Studio (version 2024.3)
    - Java version OpenJDK Runtime Environment 21.0.6

[OK] VS Code (version 1.114.0)
    - Flutter extension version 3.136.0

[OK] Connected target device used for reproduction
    - iPhone13,2
    - iOS 17.5.1 (21F90)

Activity

  1. jtmcdole commented on Jul 16, 2026

    @jtmcdole
    Member

    Flutter 3.24.5 was released 2024-11-23. We do not support the Skia backend on iOS devices.

  2. github-actions commented on Jul 30, 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.

  3. locked as resolved and limited conversation to collaborators on Jul 30, 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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions