Repository navigation
Description
Attachments
Summary
SharedThreadMerger::DecrementLease() can abort on lease_term_ref > 0 when an engine resumes drawing after its lease expires while another engine keeps the shared raster/platform queues merged.
This issue isolates the zero-lease assertion from the broader PlatformView problems in #153211. It does not cover the separate deadlock or foreground/background symptoms. Proposed fix: #194200.
Steps to reproduce
Use two RasterThreadMerger instances sharing one SharedThreadMerger and the same distinct platform/raster task queues:
- A calls MergeWithLease(1).
- A calls DecrementLease(). Its lease reaches zero and the queues unmerge, but its map entry remains.
- B calls MergeWithLease(2), merging the shared queues again.
- A resumes drawing and calls DecrementLease(). The shared merged-state check succeeds because B has an active lease.
- A's stale zero-valued entry triggers the fatal assertion.
The corresponding Android scenario uses engines in the same FlutterEngineGroup, with B's PlatformView keeping the queues merged while A resumes rendering. The affected implementation is shared C++ code; the iOS demo from #153211 has not been validated with the patch.
Android device reproduction
Confirmed on an Android test device running Android 17, using Flutter 3.35.3, Engine ddf47dd3ff, debug mode, and Impeller Vulkan. The attached demo uses two FlutterActivity instances in the same FlutterEngineGroup and a native TextView in hybrid composition; no WebView plugin is required.
- On the bottom page, press ADD PlatformView and wait for the 40-frame sequence.
- Press REMOVE PlatformView and wait for the next 40-frame sequence.
- OPEN the top Activity containing another PlatformView.
- Return to the bottom page and repeat ADD, REMOVE, OPEN in the same app process if needed.
Two initial automated cycles did not crash. A subsequent manual cycle triggered SIGABRT with the exact zero-lease assertion. The observed crash occurred while opening the top Activity again, after top onPostResume and before pressing return. Reproduction is timing-dependent; no reproduction rate is claimed. The demo uses an ordinary opaque top Activity, not a translucent variant.
Observed event sequence:
Bottom ADD completed
Bottom REMOVE and 40-frame sequence completed
Top onPostResume
FATAL shared_thread_merger.cc(59): lease_term_ref > 0, lease_term=0
The attachments above contain the source, build/run instructions, app-specific crash stack, and lifecycle timeline. Patched Android validation results are included in the validation attachment and in #194200; iOS application validation remains pending.
Expected results
Drawing with an expired lease must not abort or consume another caller's lease. Queues should unmerge when the final active lease expires.
Actual results
[FATAL:flutter/fml/shared_thread_merger.cc(59)] Check failed: lease_term_ref > 0. lease_term should always be positive when merged, lease_term=0
Root cause
DecrementLease() retains zero-valued entries in lease_term_by_caller_. RasterThreadMerger::IsMergedUnSafe() checks the shared merge state rather than whether the current caller has an active lease. Thus an expired caller can reach the decrement while another caller remains active.
Production workaround
In our Android return-to-bottom scenario, explicitly destroying the outgoing/top Flutter engine in the Activity's finish() method, instead of waiting for onDestroy(), avoids this crash. We have validated this application-level workaround in production.
This changes teardown timing for the transition where the bottom Activity can resume drawing before the top Activity reaches onDestroy(). It is a workaround for the affected lifecycle sequence; the stale zero-lease record still needs to be fixed in the Engine. Production validation here applies to the workaround. Patched Android device validation is documented in #194200; iOS application validation remains pending.
Reproduction and validation
The deterministic C++ regression is in #194200: RasterThreadMerger.ExpiredLeaseCanDecrementWhileAnotherCallerIsMerged. A standalone reproduction linked against the real FML merger and task queues aborts before the fix and passes afterward.
Unpatched source revision: 87d56361e2246bea8e40441403feda5ceb64a631.
Patched Engine validation on macOS arm64, host_debug_unopt_arm64:
- New regression: 1 passed.
- RasterThreadMerger.*: 17 passed.
- Full fml_unittests: 204 tests across 38 suites passed.
Patched Android: 30/30 cycles completed across three app processes without the target assertion or process restarts, with the early-destroy workaround disabled. Same-framework unpatched/patched device A/B and iOS application validation remain pending.
Activity
suryaKommana2662 commentedon Oct 16, 2026
@bpleekm Thanks for the detailed report and reproduction steps. I see that PR #194200 contains the proposed fix. Routing this to the team-engine for further review.
Fix/shared thread merger expired lease (flutter#194200)
Fix/shared thread merger expired lease (flutter#194200)
github-actions commentedon Nov 13, 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.