Skip to content

Commit 1023664

Browse files
authored
[Embedder] Detect and ignore stale task runner tasks (#163129)
Fixes flutter/flutter#163104 The core issue is that `EmbedderTaskRunner` can schedule things in advance, but there is no checking if the task is stale when executing it. It is possible that `FlutterEngineRunTask` will attempt to run the task on engine that is non null but already being deallocated. This in not a problem for raster and platform task runners, because raster task runner shouldn't have any scheduled tasks after shutdown and platform task runner executes the shutdown process, so the pointer is always either valid or null, but it is an issue for custom `ui_task_runner`, because it may be calling `FlutterEngineRunTask` with engine pointer that is not yet null but already shutting down. The proposed solution is to assign a unique identifier for each `EmbedderTaskRunner`, use this identifier as the `runner` field inside `FlutterTask` instead of casting the address of the runner, and verify that this identifier is valid inside `FlutterEngineRunTask` before calling anything on the engine. Special care needs to be done to not invalidate the unique identifier while `ui_task_runner` is running a task as it may lead to raciness. See `EmbedderEngine::CollectThreadHost()`. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent ef927e8 commit 1023664

8 files changed

Lines changed: 158 additions & 8 deletions

File tree

engine/src/flutter/shell/platform/embedder/embedder.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2566,6 +2566,7 @@ FlutterEngineResult FlutterEngineDeinitialize(FLUTTER_API_SYMBOL(FlutterEngine)
25662566
auto embedder_engine = reinterpret_cast<flutter::EmbedderEngine*>(engine);
25672567
embedder_engine->NotifyDestroyed();
25682568
embedder_engine->CollectShell();
2569+
embedder_engine->CollectThreadHost();
25692570
return kSuccess;
25702571
}
25712572

@@ -3226,6 +3227,13 @@ FlutterEngineResult FlutterEngineRunTask(FLUTTER_API_SYMBOL(FlutterEngine)
32263227
return LOG_EMBEDDER_ERROR(kInvalidArguments, "Invalid engine handle.");
32273228
}
32283229

3230+
if (!flutter::EmbedderThreadHost::RunnerIsValid(
3231+
reinterpret_cast<intptr_t>(task->runner))) {
3232+
// This task came too late, the embedder has already been destroyed.
3233+
// This is not an error, just ignore the task.
3234+
return kSuccess;
3235+
}
3236+
32293237
return reinterpret_cast<flutter::EmbedderEngine*>(engine)->RunTask(task)
32303238
? kSuccess
32313239
: LOG_EMBEDDER_ERROR(kInvalidArguments,

engine/src/flutter/shell/platform/embedder/embedder_engine.cc

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,46 @@ bool EmbedderEngine::CollectShell() {
6565
return IsValid();
6666
}
6767

68+
void EmbedderEngine::CollectThreadHost() {
69+
if (!thread_host_) {
70+
return;
71+
}
72+
73+
// Once the collected, EmbedderThreadHost::RunnerIsValid will return false for
74+
// all runners belonging to this thread host. This must be done with UI task
75+
// runner blocked to prevent possible raciness that could happen when
76+
// destroying the thread host in the middle of UI task runner execution. This
77+
// is not an issue for other runners, because raster task runner should not
78+
// have anything scheduled after engine shutdown and platform task runner is
79+
// where this method is called from.
80+
if (thread_host_->GetTaskRunners().GetUITaskRunner() &&
81+
!thread_host_->GetTaskRunners()
82+
.GetUITaskRunner()
83+
->RunsTasksOnCurrentThread()) {
84+
fml::AutoResetWaitableEvent ui_thread_running;
85+
fml::AutoResetWaitableEvent ui_thread_block;
86+
fml::AutoResetWaitableEvent ui_thread_finished;
87+
88+
thread_host_->GetTaskRunners().GetUITaskRunner()->PostTask([&] {
89+
ui_thread_running.Signal();
90+
ui_thread_block.Wait();
91+
ui_thread_finished.Signal();
92+
});
93+
94+
// Wait until the task is running on the UI thread.
95+
ui_thread_running.Wait();
96+
thread_host_->InvalidateActiveRunners();
97+
ui_thread_block.Signal();
98+
99+
// Needed to keep ui_thread_block in scope until the UI thread execution
100+
// finishes.
101+
ui_thread_finished.Wait();
102+
} else {
103+
thread_host_->InvalidateActiveRunners();
104+
}
105+
thread_host_.reset();
106+
}
107+
68108
bool EmbedderEngine::RunRootIsolate() {
69109
if (!IsValid() || !run_configuration_.IsValid()) {
70110
return false;
@@ -96,6 +136,7 @@ bool EmbedderEngine::NotifyDestroyed() {
96136
}
97137

98138
shell_->GetPlatformView()->NotifyDestroyed();
139+
99140
return true;
100141
}
101142

@@ -243,7 +284,7 @@ bool EmbedderEngine::RunTask(const FlutterTask* task) {
243284
if (task == nullptr) {
244285
return false;
245286
}
246-
auto result = thread_host_->PostTask(reinterpret_cast<int64_t>(task->runner),
287+
auto result = thread_host_->PostTask(reinterpret_cast<intptr_t>(task->runner),
247288
task->task);
248289
// If the UI and platform threads are separate, the microtask queue is
249290
// flushed through MessageLoopTaskQueues observer.

engine/src/flutter/shell/platform/embedder/embedder_engine.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class EmbedderEngine {
3838

3939
bool CollectShell();
4040

41+
void CollectThreadHost();
42+
4143
const TaskRunners& GetTaskRunners() const;
4244

4345
bool NotifyCreated();
@@ -88,7 +90,7 @@ class EmbedderEngine {
8890
Shell& GetShell();
8991

9092
private:
91-
const std::unique_ptr<EmbedderThreadHost> thread_host_;
93+
std::unique_ptr<EmbedderThreadHost> thread_host_;
9294
TaskRunners task_runners_;
9395
RunConfiguration run_configuration_;
9496
std::unique_ptr<ShellArgs> shell_args_;

engine/src/flutter/shell/platform/embedder/embedder_task_runner.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99

1010
namespace flutter {
1111

12+
std::atomic_intptr_t EmbedderTaskRunner::next_unique_id_ = 0;
13+
1214
EmbedderTaskRunner::EmbedderTaskRunner(DispatchTable table,
1315
size_t embedder_identifier)
1416
: TaskRunner(nullptr /* loop implemenation*/),
1517
embedder_identifier_(embedder_identifier),
1618
dispatch_table_(std::move(table)),
17-
placeholder_id_(fml::TaskQueueId(fml::TaskQueueId::kInvalid)) {
19+
placeholder_id_(fml::TaskQueueId(fml::TaskQueueId::kInvalid)),
20+
unique_id_(next_unique_id_++) {
1821
FML_DCHECK(dispatch_table_.post_task_callback);
1922
FML_DCHECK(dispatch_table_.runs_task_on_current_thread_callback);
2023
FML_DCHECK(dispatch_table_.destruction_callback);

engine/src/flutter/shell/platform/embedder/embedder_task_runner.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,18 @@ class EmbedderTaskRunner final : public fml::TaskRunner {
7575

7676
bool PostTask(uint64_t baton);
7777

78+
intptr_t unique_id() const { return unique_id_; }
79+
7880
private:
7981
const size_t embedder_identifier_;
8082
DispatchTable dispatch_table_;
8183
std::mutex tasks_mutex_;
8284
uint64_t last_baton_ = 0;
8385
std::unordered_map<uint64_t, fml::closure> pending_tasks_;
8486
fml::TaskQueueId placeholder_id_;
87+
intptr_t unique_id_;
88+
89+
static std::atomic_intptr_t next_unique_id_;
8590

8691
// |fml::TaskRunner|
8792
void PostTask(const fml::closure& task) override;

engine/src/flutter/shell/platform/embedder/embedder_thread_host.cc

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace flutter {
1515

16+
std::set<intptr_t> EmbedderThreadHost::active_runners_;
17+
std::mutex EmbedderThreadHost::active_runners_mutex_;
18+
1619
//------------------------------------------------------------------------------
1720
/// @brief Attempts to create a task runner from an embedder task runner
1821
/// description. The first boolean in the pair indicate whether the
@@ -67,7 +70,7 @@ CreateEmbedderTaskRunner(const FlutterTaskRunnerDescription* description) {
6770
fml::TimePoint target_time) -> void {
6871
FlutterTask task = {
6972
// runner
70-
reinterpret_cast<FlutterTaskRunner>(task_runner),
73+
reinterpret_cast<FlutterTaskRunner>(task_runner->unique_id()),
7174
// task
7275
task_baton,
7376
};
@@ -306,13 +309,27 @@ EmbedderThreadHost::EmbedderThreadHost(
306309
const flutter::TaskRunners& runners,
307310
const std::set<fml::RefPtr<EmbedderTaskRunner>>& embedder_task_runners)
308311
: host_(std::move(host)), runners_(runners) {
312+
std::lock_guard guard(active_runners_mutex_);
309313
for (const auto& runner : embedder_task_runners) {
310-
runners_map_[reinterpret_cast<int64_t>(runner.get())] = runner;
314+
runners_map_[runner->unique_id()] = runner;
315+
active_runners_.insert(runner->unique_id());
311316
}
312317
}
313318

314319
EmbedderThreadHost::~EmbedderThreadHost() = default;
315320

321+
void EmbedderThreadHost::InvalidateActiveRunners() {
322+
std::lock_guard guard(active_runners_mutex_);
323+
for (const auto& runner : runners_map_) {
324+
active_runners_.erase(runner.first);
325+
}
326+
}
327+
328+
bool EmbedderThreadHost::RunnerIsValid(intptr_t runner) {
329+
std::lock_guard guard(active_runners_mutex_);
330+
return active_runners_.find(runner) != active_runners_.end();
331+
}
332+
316333
bool EmbedderThreadHost::IsValid() const {
317334
return runners_.IsValid();
318335
}
@@ -321,7 +338,7 @@ const flutter::TaskRunners& EmbedderThreadHost::GetTaskRunners() const {
321338
return runners_;
322339
}
323340

324-
bool EmbedderThreadHost::PostTask(int64_t runner, uint64_t task) const {
341+
bool EmbedderThreadHost::PostTask(intptr_t runner, uint64_t task) const {
325342
auto found = runners_map_.find(runner);
326343
if (found == runners_map_.end()) {
327344
return false;

engine/src/flutter/shell/platform/embedder/embedder_thread_host.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,19 @@ class EmbedderThreadHost {
3636

3737
const flutter::TaskRunners& GetTaskRunners() const;
3838

39-
bool PostTask(int64_t runner, uint64_t task) const;
39+
bool PostTask(intptr_t runner, uint64_t task) const;
40+
41+
static bool RunnerIsValid(intptr_t runner);
42+
43+
void InvalidateActiveRunners();
4044

4145
private:
4246
ThreadHost host_;
4347
flutter::TaskRunners runners_;
44-
std::map<int64_t, fml::RefPtr<EmbedderTaskRunner>> runners_map_;
48+
std::map<intptr_t, fml::RefPtr<EmbedderTaskRunner>> runners_map_;
49+
50+
static std::set<intptr_t> active_runners_;
51+
static std::mutex active_runners_mutex_;
4552

4653
static std::unique_ptr<EmbedderThreadHost> CreateEmbedderManagedThreadHost(
4754
const FlutterCustomTaskRunners* custom_task_runners,

engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,73 @@ TEST_F(EmbedderTest, CanSpecifyCustomUITaskRunner) {
269269
kill_latch.Wait();
270270
}
271271

272+
TEST_F(EmbedderTest, IgnoresStaleTasks) {
273+
auto& context = GetEmbedderContext<EmbedderTestContextSoftware>();
274+
auto ui_task_runner = CreateNewThread("test_ui_thread");
275+
auto platform_task_runner = CreateNewThread("test_platform_thread");
276+
static std::mutex engine_mutex;
277+
UniqueEngine engine;
278+
FlutterEngine engine_ptr;
279+
280+
EmbedderTestTaskRunner test_ui_task_runner(
281+
ui_task_runner, [&](FlutterTask task) {
282+
// The check for engine.is_valid() is intentionally absent here.
283+
// FlutterEngineRunTask must be able to detect and ignore stale tasks
284+
// without crashing even if the engine pointer is not null.
285+
// Because the engine is destroyed on platform thread,
286+
// relying solely on engine.is_valid() in UI thread is not safe.
287+
FlutterEngineRunTask(engine_ptr, &task);
288+
});
289+
EmbedderTestTaskRunner test_platform_task_runner(
290+
platform_task_runner, [&](FlutterTask task) {
291+
std::scoped_lock lock(engine_mutex);
292+
if (!engine.is_valid()) {
293+
return;
294+
}
295+
FlutterEngineRunTask(engine.get(), &task);
296+
});
297+
298+
fml::AutoResetWaitableEvent init_latch;
299+
300+
platform_task_runner->PostTask([&]() {
301+
EmbedderConfigBuilder builder(context);
302+
const auto ui_task_runner_description =
303+
test_ui_task_runner.GetFlutterTaskRunnerDescription();
304+
const auto platform_task_runner_description =
305+
test_platform_task_runner.GetFlutterTaskRunnerDescription();
306+
builder.SetUITaskRunner(&ui_task_runner_description);
307+
builder.SetPlatformTaskRunner(&platform_task_runner_description);
308+
{
309+
std::scoped_lock lock(engine_mutex);
310+
engine = builder.InitializeEngine();
311+
}
312+
init_latch.Signal();
313+
});
314+
315+
init_latch.Wait();
316+
engine_ptr = engine.get();
317+
318+
auto flutter_engine = reinterpret_cast<EmbedderEngine*>(engine.get());
319+
320+
// Schedule task on UI thread that will likely run after the engine has shut
321+
// down.
322+
flutter_engine->GetTaskRunners().GetUITaskRunner()->PostDelayedTask(
323+
[]() {}, fml::TimeDelta::FromMilliseconds(50));
324+
325+
fml::AutoResetWaitableEvent kill_latch;
326+
platform_task_runner->PostTask([&] {
327+
engine.reset();
328+
platform_task_runner->PostTask([&kill_latch] { kill_latch.Signal(); });
329+
});
330+
kill_latch.Wait();
331+
332+
// Ensure that the schedule task indeed runs.
333+
kill_latch.Reset();
334+
ui_task_runner->PostDelayedTask([&]() { kill_latch.Signal(); },
335+
fml::TimeDelta::FromMilliseconds(50));
336+
kill_latch.Wait();
337+
}
338+
272339
TEST_F(EmbedderTest, MergedPlatformUIThread) {
273340
auto& context = GetEmbedderContext<EmbedderTestContextSoftware>();
274341
auto task_runner = CreateNewThread("test_thread");

0 commit comments

Comments
 (0)