Skip to content

Fix SIGSEGV in VSyncClient on ProMotion devices when engine shell is nil - #184639

Closed
realmeylisdev wants to merge 7 commits into
flutter:masterfrom
realmeylisdev:fix/183900-vsync-client-null-guard
Closed

realmeylisdev wants to merge 7 commits into
flutter:masterfrom
realmeylisdev:fix/183900-vsync-client-null-guard

Conversation

@realmeylisdev

Copy link
Copy Markdown
Contributor

Summary

Fixes #183900

  • On iOS 26 with the implicit engine pattern, viewDidLoad fires before the engine shell is initialized. createTouchRateCorrectionVSyncClientIfNeeded passes an empty fml::RefPtr to VSyncClient initWithTaskRunner:callback:, which dereferences the null pointer at task_runner->PostTask(...). Only affects ProMotion devices (>60Hz) because ≤60Hz returns early.
  • Adds defense-in-depth null checks at three levels: VSyncClient initWithTaskRunner:callback: returns nil for null task_runner, createTouchRateCorrectionVSyncClientIfNeeded guards platformTaskRunner, and setUpKeyboardAnimationVsyncClient: guards uiTaskRunner.
  • Includes 3 new unit tests covering the null task runner paths.

Files changed

File Change
vsync_waiter_ios.mm Null guard in initWithTaskRunner:callback:
FlutterViewController.mm Null guards in createTouchRateCorrectionVSyncClientIfNeeded and setUpKeyboardAnimationVsyncClient:
VsyncWaiterIosTest.mm 1 new test: testInitWithNullTaskRunnerReturnsNil
FlutterViewControllerTest.mm 2 new tests: no-crash tests for shell-less engine

Test plan

  • Run engine iOS unit tests (VsyncWaiterIosTest + FlutterViewControllerTest)
  • Manual test on ProMotion device without debugger using implicit engine pattern
  • Verify no regression on non-ProMotion (≤60Hz) devices

…nil (flutter#183900)

On iOS 26 with the implicit engine pattern, `viewDidLoad` can fire
before the engine shell is fully initialized. `createTouchRateCorrectionVSyncClientIfNeeded`
passes `self.engine.platformTaskRunner` (an empty `fml::RefPtr` when
`_shell` is nil) to VSyncClient, which dereferences the null pointer at
`task_runner->PostTask(...)`. Only hits ProMotion devices because ≤60Hz
returns early before reaching the VSyncClient creation.

Defense-in-depth null checks at three levels:
- VSyncClient `initWithTaskRunner:callback:` returns nil for null task_runner
- `createTouchRateCorrectionVSyncClientIfNeeded` guards platformTaskRunner
- `setUpKeyboardAnimationVsyncClient:` guards uiTaskRunner

Includes 3 new unit tests covering the null task runner paths.
@realmeylisdev
realmeylisdev requested a review from a team as a code owner April 5, 2026 16:06
@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 Apr 5, 2026

@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 introduces null checks for platform and UI task runners in FlutterViewController and VSyncClient to prevent crashes when the engine lacks a shell. It also includes unit tests to verify these scenarios. Feedback suggests removing a conflicting FML_DCHECK that would cause debug builds to crash despite the new null handling, and adding stopMocking calls to the new tests to ensure proper mock cleanup and isolation.

- (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
callback:(flutter::VsyncWaiter::Callback)callback {
FML_DCHECK(task_runner);
if (!task_runner) {

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.

high

The FML_DCHECK(task_runner) on line 73 will cause debug builds and the new unit test testInitWithNullTaskRunnerReturnsNil to crash when a null task runner is provided. Since this PR explicitly adds support for handling a null task runner by returning nil, the FML_DCHECK should be removed to allow the code to function as intended in debug environments.

Comment on lines +2679 to +2691
- (void)testCreateTouchRateCorrectionVSyncClientDoesNotCrashWhenEngineHasNoShell {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
// Create engine without running it — shell will be nil, so platformTaskRunner returns empty RefPtr.
FlutterEngine* engine = [[FlutterEngine alloc] init];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
// Should not crash even though engine has no shell.
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
}

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

When mocking a class with OCMock, it is important to call stopMocking on the mock object at the end of the test. This ensures that the class is restored to its original state and prevents the mock behavior from leaking into other tests.

- (void)testCreateTouchRateCorrectionVSyncClientDoesNotCrashWhenEngineHasNoShell {
  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
  double maxFrameRate = 120;
  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
  // Create engine without running it — shell will be nil, so platformTaskRunner returns empty RefPtr.
  FlutterEngine* engine = [[FlutterEngine alloc] init];
  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
                                                                                nibName:nil
                                                                                 bundle:nil];
  // Should not crash even though engine has no shell.
  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
  XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
  [mockDisplayLinkManager stopMocking];
}
References
  1. Code should follow the guidance described in the writing effective tests guide, which includes proper cleanup of mocks to ensure test isolation. (link)

realmeylisdev and others added 3 commits April 6, 2026 11:11
…ng to test

- Remove FML_DCHECK(task_runner) that would crash debug builds before
  the null check could return nil
- Add [mockDisplayLinkManager stopMocking] to prevent mock leakage
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 9, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 14, 2026
@LouiseHsu LouiseHsu added the CICD Run CI/CD label Apr 15, 2026
@okorohelijah

Copy link
Copy Markdown
Contributor

@realmeylisdev from triage: can you address the failing checks?

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 17, 2026
@LouiseHsu LouiseHsu added the CICD Run CI/CD label Apr 17, 2026
@hellohuanlin

Copy link
Copy Markdown
Contributor

From triage: test are failing and it's not addressed, so we are closing it. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS 26 Beta] Crash (SIGSEGV) in -[VSyncClient initWithTaskRunner:callback:] on ProMotion devices using implicit engine

5 participants