Sitelet https://github.com/flutter/flutter/pull/192465
Skip to content

ios: Return NO from run* calls on a running/terminated engine - #192465

Merged
cbracken merged 2 commits into
flutter:masterfrom
cbracken:fix-incorrect-run-return-value
Sep 10, 2026
Merged

cbracken merged 2 commits into
flutter:masterfrom
cbracken:fix-incorrect-run-return-value

Conversation

@cbracken

@cbracken cbracken commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

We explicitly document in FlutterEngine's run* methods that calling run more than once is will return without doing anything. In [FlutterEngine destroyContext] we specifically document that the engine is in an unusable state, that the engine will be in an unusable state until it is deallocated, and that accessing properties or sending messages to it will result in undefined behavior or runtime errors.

By design, all calls to run* methods after the first invocation returned NO. This was unintentionally changed in flutter-team-archive/engine#6774 such that calling run on an already-running engine returns YES instead of false as it was prior to that patch. That's incorrect behaviour since it suggests that a new engine was spun up. A particularly obvious incorrect behaviour that this triggered was returning YES in the second invocation in a case like this:

[engine runWithEntryPoint:@"main" ...];
[engine runWithEntryPoint:@"notMain" ...];

This restores the original behaviour and prevents spinning up a new shell on a destroyed engine. We only ever documented that calling run a second time was unsupported and would return immediately with no effect, but never documented the return value on a second invocation, this updates that to document (and test) the intended behaviour.

This also makes destroyContext idempotent such that calling it a second time is a no-op. This can happen if destroyContext is called directly as well as via e.g. appOrSceneWillTerminate or on view controller deallocation.

This also adds null guards to unregisterTexture: and textureFrameAvailable: which would previously have triggered a null dereference.

Related: flutter-team-archive/engine#6774
Related: flutter-team-archive/engine#7610

Fixes: #185608
Fixes: #115157 (b/256086107)
Issue: #112232

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@cbracken
cbracken requested a review from a team as a code owner September 9, 2026 07:43
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Sep 9, 2026
@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. team-ios Owned by iOS platform team labels Sep 9, 2026
@cbracken
cbracken requested review from gaaclarke and knopp September 9, 2026 07:44

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates FlutterEngine to prevent running the engine again after it has been destroyed, making destroyContext idempotent and adding safety checks to ignore texture callbacks after destruction. It also updates the documentation and adds comprehensive unit tests for these behaviors. The review feedback suggests caching self.platformView in a local variable within -unregisterTexture: and -textureFrameAvailable: to avoid redundant getter calls and protect against potential TOCTOU issues.

Comment on lines 1438 to 1443
- (void)unregisterTexture:(int64_t)textureId {
_shell->GetPlatformView()->UnregisterTexture(textureId);
if (!self.platformView) {
return;
}
self.platformView->UnregisterTexture(textureId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent redundant getter calls and protect against potential TOCTOU (Time-of-Check to Time-of-Use) issues if self.platformView is modified or cleared concurrently, consider caching the platform view pointer in a local variable.

- (void)unregisterTexture:(int64_t)textureId {
  auto* platformView = self.platformView;
  if (!platformView) {
    return;
  }
  platformView->UnregisterTexture(textureId);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not TOCTOU issue here. Both this and destroyContext run on the platform thread. They can't be interleaved.

That said I'm doing it anyway cause self.platformView does a null check fml::WeakPtr get() so there's no sense in doing that twice.

Comment on lines 1445 to 1450
- (void)textureFrameAvailable:(int64_t)textureId {
_shell->GetPlatformView()->MarkTextureFrameAvailable(textureId);
if (!self.platformView) {
return;
}
self.platformView->MarkTextureFrameAvailable(textureId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, cache the platform view pointer in a local variable to avoid redundant getter calls and potential TOCTOU issues.

- (void)textureFrameAvailable:(int64_t)textureId {
  auto* platformView = self.platformView;
  if (!platformView) {
    return;
  }
  platformView->MarkTextureFrameAvailable(textureId);
}

@cbracken cbracken Sep 9, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case you're right about the TOCTOU behaviour since this callback happens on the raster thread.

Edit: nope my memory is wrong we're on the platform thread here too.

void Shell::OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) {
  FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread());

Alright updated:

There is not TOCTOU issue here. Both this and destroyContext run on the platform thread. They can't be interleaved.

That said I'm doing it anyway cause self.platformView does a null check fml::WeakPtr get() so there's no sense in doing that twice.

Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm Outdated
We explicitly document in FlutterEngine's `run*` methods that calling
run more than once is will return without doing anything. In
`[FlutterEngine destroyContext]` we specifically document that the
engine is in an unusable state, that the engine will be in an unusable
state until it is deallocated, and that accessing properties or sending
messages to it will result in undefined behavior or runtime errors.

By design, all calls to `run*` methods after the first invocation
returned `NO`. This was unintentionally changed in
flutter-team-archive/engine#6774 such that calling `run` on an already-running
engine returns `YES` instead of `false` as it was prior to that patch.
That's incorrect behaviour since it suggests that a new engine was spun
up. A particularly obvious incorrect behaviour that this triggered was
returning `YES` in the second invocation in a case like this:

```objc
[engine runWithEntryPoint:@"main" ...];
[engine runWithEntryPoint:@"notMain" ...];
```

This restores the original behaviour and prevents spinning up a new
shell on a destroyed engine. We only ever documented that calling `run`
a second time was unsupported and would return immediately with no
effect, but never documented the return value on a second invocation,
this updates that to document (and test) the intended behaviour.

This also makes destroyContext idempotent such that calling it a second
time is a no-op. This can happen if destroyContext is called directly
as well as via e.g. `appOrSceneWillTerminate` or on view controller
deallocation.

This also adds null guards to unregisterTexture: and
textureFrameAvailable: which would previously have triggered a
null dereference.

Related: flutter-team-archive/engine#6774
Related: flutter-team-archive/engine#7610

Issue: flutter#185608
Issue: flutter#115157 (b/256086107)
@cbracken
cbracken force-pushed the fix-incorrect-run-return-value branch from 72ced12 to 087e77c Compare September 9, 2026 11:34
@github-actions github-actions Bot added a: desktop Running on desktop team-macos Owned by the macOS platform team labels Sep 9, 2026
@flutter-dashboard

Copy link
Copy Markdown

CI had a failure that stopped further tests from running. We need to investigate to determine the root cause.

SHA at time of execution: 087e77c.

Possible causes:

  • Configuration Changes: The .ci.yaml file might have been modified between the creation of this pull request and the start of this test run. This can lead to ci yaml validation errors.
  • Infrastructure Issues: Problems with the CI environment itself (e.g., quota) could have caused the failure.

A blank commit, or merging to head, will be required to resume running CI for this PR.

Error Details:

DetailedApiRequestError(status: 409, message: Document already exists: projects/flutter-dashboard/databases/cocoon/documents/presubmit_jobs/flutter_flutter_102448658497_Linux analyze_1)

Stack trace:

#0      validateResponse (package:_discoveryapis_commons/src/api_requester.dart:323:9)
<asynchronous suspension>
#1      ApiRequester.request (package:_discoveryapis_commons/src/api_requester.dart:82:16)
<asynchronous suspension>
#2      ProjectsDatabasesDocumentsResource.commit (package:googleapis/firestore/v1.dart:1358:23)
<asynchronous suspension>
#3      UnifiedCheckRun.initializeCiStagingDocument (package:cocoon_service/src/service/firestore/unified_check_run.dart:74:7)
<asynchronous suspension>
#4      Scheduler._runCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1472:7)
<asynchronous suspension>
#5      Scheduler.proceedToCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1554:7)
<asynchronous suspension>
#6      Scheduler._closeSuccessfulEngineBuildStage (package:cocoon_service/src/service/scheduler.dart:1351:5)
<asynchronous suspension>
#7      Scheduler.processCheckRunCompleted (package:cocoon_service/src/service/scheduler.dart:1277:11)
<asynchronous suspension>
#8      PresubmitSubscription._processBuild (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:226:7)
<asynchronous suspension>
#9      PresubmitSubscription.post (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:119:5)
<asynchronous suspension>
#10     RequestHandler.service (package:cocoon_service/src/request_handling/request_handler.dart:42:20)
<asynchronous suspension>
#11     SubscriptionHandler.service (package:cocoon_service/src/request_handling/subscription_handler.dart:134:5)
<asynchronous suspension>
#12     createServer.<anonymous closure> (package:cocoon_service/server.dart:462:7)
<asynchronous suspension>
#13     main.<anonymous closure>.<anonymous closure> (file:///app/app_dart/bin/gae_server.dart:192:9)
<asynchronous suspension>

@flutter-dashboard

Copy link
Copy Markdown

CI had a failure that stopped further tests from running. We need to investigate to determine the root cause.

SHA at time of execution: 087e77c.

Possible causes:

  • Configuration Changes: The .ci.yaml file might have been modified between the creation of this pull request and the start of this test run. This can lead to ci yaml validation errors.
  • Infrastructure Issues: Problems with the CI environment itself (e.g., quota) could have caused the failure.

A blank commit, or merging to head, will be required to resume running CI for this PR.

Error Details:

DetailedApiRequestError(status: 409, message: Document already exists: projects/flutter-dashboard/databases/cocoon/documents/presubmit_jobs/flutter_flutter_102448658497_Linux analyze_1)

Stack trace:

#0      validateResponse (package:_discoveryapis_commons/src/api_requester.dart:323:9)
<asynchronous suspension>
#1      ApiRequester.request (package:_discoveryapis_commons/src/api_requester.dart:82:16)
<asynchronous suspension>
#2      ProjectsDatabasesDocumentsResource.commit (package:googleapis/firestore/v1.dart:1358:23)
<asynchronous suspension>
#3      UnifiedCheckRun.initializeCiStagingDocument (package:cocoon_service/src/service/firestore/unified_check_run.dart:74:7)
<asynchronous suspension>
#4      Scheduler._runCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1472:7)
<asynchronous suspension>
#5      Scheduler.proceedToCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1554:7)
<asynchronous suspension>
#6      Scheduler._closeSuccessfulEngineBuildStage (package:cocoon_service/src/service/scheduler.dart:1351:5)
<asynchronous suspension>
#7      Scheduler.processCheckRunCompleted (package:cocoon_service/src/service/scheduler.dart:1277:11)
<asynchronous suspension>
#8      PresubmitSubscription._processBuild (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:226:7)
<asynchronous suspension>
#9      PresubmitSubscription.post (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:119:5)
<asynchronous suspension>
#10     RequestHandler.service (package:cocoon_service/src/request_handling/request_handler.dart:42:20)
<asynchronous suspension>
#11     SubscriptionHandler.service (package:cocoon_service/src/request_handling/subscription_handler.dart:134:5)
<asynchronous suspension>
#12     createServer.<anonymous closure> (package:cocoon_service/server.dart:462:7)
<asynchronous suspension>
#13     main.<anonymous closure>.<anonymous closure> (file:///app/app_dart/bin/gae_server.dart:192:9)
<asynchronous suspension>

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, one question


return _shell != nullptr;
[self launchEngine:entrypoint libraryURI:libraryURI entrypointArgs:entrypointArgs];
return YES;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we still want return _shell != nullptr;? I guessing that checks problems with launchEngine

@cbracken cbracken Sep 9, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If shell is nullptr, [FlutterEngine launchEngine:libraryURI:entryPointArgs:] already crashes; it's got an FML_DCHECK and it dereferences it:

flutter::RunConfiguration configuration =
[self.dartProject runConfigurationForEntrypoint:entrypoint
libraryOrNil:libraryOrNil
entrypointArgs:entrypointArgs];
configuration.SetEngineId(self.engineIdentifier);
self.shell.RunEngine(std::move(configuration));

The good news is, it won't be nullptr... because if we fail to launch, we don't actually reset shell, so it's not a good indicator of success either way:

auto run_result = weak_engine->Run(std::move(run_configuration));
if (run_result == flutter::Engine::RunStatus::Failure) {
FML_LOG(ERROR) << "Could not launch engine with configuration.";
}

I guess the more interesting point is that we currently have no mechanism in the code to let the embedder know that launch failed. We should probably fix that but it's probably out of scope for this guy since that might happen on another thread (the UI thread). For iOS we have merged threads so this should be more than possible.

I can send a followup that deals with that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, a followup sounds good to me. A FML_DCHECK will only catch in debug builds. Can you please file a brief followup issue. No need to add a TODO comment in the code unless you want to.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FML_DCHECK will only fire in debug builds but dereferencing the pointer on the next line will fire in release builds :)

Will file an issue and send a patch.

@flutter-dashboard

Copy link
Copy Markdown

CI had a failure that stopped further tests from running. We need to investigate to determine the root cause.

SHA at time of execution: 087e77c.

Possible causes:

  • Configuration Changes: The .ci.yaml file might have been modified between the creation of this pull request and the start of this test run. This can lead to ci yaml validation errors.
  • Infrastructure Issues: Problems with the CI environment itself (e.g., quota) could have caused the failure.

A blank commit, or merging to head, will be required to resume running CI for this PR.

Error Details:

DetailedApiRequestError(status: 409, message: Document already exists: projects/flutter-dashboard/databases/cocoon/documents/presubmit_jobs/flutter_flutter_102448658497_Linux analyze_1)

Stack trace:

#0      validateResponse (package:_discoveryapis_commons/src/api_requester.dart:323:9)
<asynchronous suspension>
#1      ApiRequester.request (package:_discoveryapis_commons/src/api_requester.dart:82:16)
<asynchronous suspension>
#2      ProjectsDatabasesDocumentsResource.commit (package:googleapis/firestore/v1.dart:1358:23)
<asynchronous suspension>
#3      UnifiedCheckRun.initializeCiStagingDocument (package:cocoon_service/src/service/firestore/unified_check_run.dart:74:7)
<asynchronous suspension>
#4      Scheduler._runCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1472:7)
<asynchronous suspension>
#5      Scheduler.proceedToCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1554:7)
<asynchronous suspension>
#6      Scheduler._closeSuccessfulEngineBuildStage (package:cocoon_service/src/service/scheduler.dart:1351:5)
<asynchronous suspension>
#7      Scheduler.processCheckRunCompleted (package:cocoon_service/src/service/scheduler.dart:1277:11)
<asynchronous suspension>
#8      PresubmitSubscription._processBuild (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:226:7)
<asynchronous suspension>
#9      PresubmitSubscription.post (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:119:5)
<asynchronous suspension>
#10     RequestHandler.service (package:cocoon_service/src/request_handling/request_handler.dart:42:20)
<asynchronous suspension>
#11     SubscriptionHandler.service (package:cocoon_service/src/request_handling/subscription_handler.dart:134:5)
<asynchronous suspension>
#12     createServer.<anonymous closure> (package:cocoon_service/server.dart:462:7)
<asynchronous suspension>
#13     main.<anonymous closure>.<anonymous closure> (file:///app/app_dart/bin/gae_server.dart:192:9)
<asynchronous suspension>

@cbracken
cbracken added this pull request to the merge queue Sep 10, 2026
Merged via the queue into flutter:master with commit 6cfe40f Sep 10, 2026
18 checks passed
@cbracken
cbracken deleted the fix-incorrect-run-return-value branch September 10, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team team-macos Owned by the macOS platform team

Projects

None yet

3 participants