Minimal reproduction — flutter/flutter#190774
Snapshotting a route transition while a GPU-surface external texture
(kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle) is on screen kills the process
under Impeller on Windows, with 0xC0000005 on a null read.
flutter pub get
flutter run -d windowsThe app registers a texture, pushes a route about half a second later, and dies
on that first transition. flutter run prints Lost connection to device. No
environment variables, no scripts, nothing else required.
To see the faulting stack instead of a silent exit, set one variable first:
$env:FLUTTER_REPRO_CRASH_TRACE = '1'
flutter run -d windowsThat enables a small vectored exception handler in the runner
(windows/runner/native_crash_reporter.cpp) which prints module+offset frames
to stderr. It is off by default and is not part of the bug.
Expected: routes push and pop for 30 seconds, the app prints
clean timeout reached; exiting normally, exits 0.
Actual: access violation on the first route push, exit 0xC0000005.
This is not a teardown artifact, and the reason is structural rather than inferred from log timing: route push/pop performs no texture retirement at all. Unregistration exists only in the native plugin's destructor, so the texture and its producer are still live and registered throughout the transition that crashes.
| Item | Value |
|---|---|
| Flutter | 3.47.0-0.1.pre (beta) |
| Framework revision | bd1e75d918605c91b411e8789fb911e6c9a84534 |
| Engine revision | bbd15867c003dc66e678cb3c218649fa8bf914f2 |
flutter_windows.dll SHA-256 |
7902667F7BE09E606AEC1A8679AD53EC02B1E25CA580F525C114FEE8AD697635 |
| OS | Windows 11 Pro build 26200 x64 |
| GPU | NVIDIA RTX 3080, driver 595.95 (32.0.15.9595) |
| Toolchain | VS 2022 17.14.36, Windows SDK 10.0.26100.0 |
| Backend | Using the Impeller rendering backend (OpenGLESSDF) |
Module offsets are only comparable within a single engine revision. Also
reproduces on 3.47.0-0.3.pre (engine effb186f31…, same offsets) and on the
current beta 3.47.0-0.4.pre (engine f88005a259…, whole chain shifted by
exactly 0x400).
The original case went through a third-party Vulkan renderer. That is replaced
with plain D3D11 in the runner (windows/runner/external_texture_repro.cpp) —
the smallest thing that still hands Flutter a DXGI shared-handle GPU surface. It
preserves the contract that mattered:
- allocates on Flutter's own DXGI adapter (
GetGraphicsAdapter) DXGI_FORMAT_B8G8R8A8_UNORMD3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLEIDXGIResource1::CreateSharedHandleGpuSurfaceTexture(kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle)- descriptor, texture variant and shared handle retained for the texture's life
MarkTextureFrameAvailablefrom a worker thread at ~60 Hz
On the Dart side a Texture widget sits under a ZoomPageTransitionsBuilder
with allowSnapshotting: true, and a loop pushes and pops a route every ~1.3
seconds. Dependencies are flutter, flutter_test, flutter_lints only; the
texture is a 512×512 surface cleared to a cycling solid colour, with no scene or
asset loading.
Synchronization, stated explicitly: the producer uses
SHARED | SHARED_NTHANDLE without a keyed mutex, calling Flush after each
clear. That is disclosed deliberately rather than left to be inferred. It is not
believed to be the cause — the stack matches the null-context path already
identified on this issue — but it is the kind of thing a reviewer should be able
to see and judge.
Texture retirement uses the asynchronous UnregisterTexture(texture_id, callback)
overload and retains the texture variant, descriptor, D3D resources and shared
handle through that callback, so deprecated synchronous teardown is not a
confound.
code : 0xc0000005
faulting addr : flutter_windows.dll+0x2c51f7
operation : READ at 0x0
...
flutter_windows.dll+0x2c51f7
flutter_windows.dll+0xeb4d1
flutter_windows.dll+0xeaf0c
flutter_windows.dll+0xeadbb
...
flutter_windows.dll+0x5450fd
Below those frames the debugger shows a deeply recursive pattern alternating between two call sites, consistent with a layer-tree walk.
flutter_windows.dll+0x5450fd is reported upstream on this issue as
LayerTree::Flatten at this engine revision. That attribution comes from the
existing symbolized analysis, not from anything in this repository — the
retained debugger log has no usable engine symbols and cannot confirm it.
Caveats: .ecxr returns Unable to get exception context, HRESULT 0x8000FFFF,
so frames come from the break-in context at the first-chance AV. There are no
public PDBs for flutter_windows.dll, so the debugger resolves every frame to
the nearest exported symbol plus a huge offset; those names are meaningless and
only module+offset is quoted. To capture a dump yourself, use .dump /mrR —
/mp includes the process environment block.
Everything above needs only flutter run. run_repro.ps1 exists to run the
controls reproducibly and record evidence; it is tooling, not part of the bug.
.\run_repro.ps1 -DurationSeconds 30 -Clean| Snapshotting | Continuous updates/marks | Impeller | Result | Exit code |
|---|---|---|---|---|
| on | on | on | crash, signature matched | 0xC0000005 |
| off | on | on | control passed, 22 cycles | 0 |
| on | off | on | control passed, 22 cycles | 0 |
| on | on | off | control passed, 22 cycles | 0 |
Each run used a 30-second configured timeout; the crashing run ends earlier.
Each control completed 22 push/pop cycles, so they exercised the same path
rather than failing to start, and the Impeller-off log contains zero
Using the Impeller rendering backend lines. Logs are retained under logs\.
Add -AllowAnyEngine to test a different SDK, which classifies on fault module
and fault type rather than exact offsets.
Stated precisely: in one 30-second run per configuration, the crash was not observed when snapshotting, continuous updates/marks, or Impeller was disabled. That is weaker than "all three are required" — each dimension would need independent isolation and repeats first.
Two caveats about the -MarkFrames:$false control:
- It is not "no frame marks".
Create()always renders one frame and marks it once at registration; the flag only disables the subsequent ~60 Hz worker. - It changes two things at once — ongoing texture writes and ongoing frame
notifications — so it cannot distinguish new pixel content from the cache
invalidation
MarkTextureFrameAvailabletriggers.
One trap worth knowing if you write your own controls: flutter run does not
propagate FLUTTER_ENGINE_SWITCHES to the application process. An
"Impeller disabled" run launched through flutter run still starts Impeller and
still crashes, which reads as "not Impeller-specific." My first Impeller-off
control was invalid for exactly that reason. run_repro.ps1 builds and launches
the .exe directly to avoid it, and fails if -ImpellerOff is requested but
Impeller starts anyway. flutter run also reported exit code 0 after the app
had died of an access violation, so classify from the log, not the CLI.