Skip to content

Fix crash in FlutterJSONMessageCodec when decoding invalid Unicode#179775

Closed
mahmuttaskiran wants to merge 5 commits into
flutter:masterfrom
mahmuttaskiran:fix/flutterjsonmessagecodec-nil-dictionary-crash
Closed

Fix crash in FlutterJSONMessageCodec when decoding invalid Unicode#179775
mahmuttaskiran wants to merge 5 commits into
flutter:masterfrom
mahmuttaskiran:fix/flutterjsonmessagecodec-nil-dictionary-crash

Conversation

@mahmuttaskiran

@mahmuttaskiran mahmuttaskiran commented Dec 12, 2025

Copy link
Copy Markdown
Contributor

Fix crash in FlutterJSONMessageCodec when decoding invalid Unicode

Added a safety check in decodeMethodCall to handle cases where JSON decoding returns nil. This prevents the app from crashing when invalid UTF-8 is passed into the codec.

Fixing #179727

Pre-launch Checklist

talabat.com Talabat Flutter PRs

Added a safety check in decodeMethodCall to handle cases where JSON decoding
returns nil. This prevents the app from crashing when invalid UTF-8 or malformed
Unicode is passed into the codec.
@mahmuttaskiran mahmuttaskiran requested a review from a team as a code owner December 12, 2025 04:37
@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. a: desktop Running on desktop team-ios Owned by iOS platform team labels Dec 12, 2025

@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 addresses a crash that occurs in FlutterJSONMessageCodec when it attempts to decode invalid Unicode characters. The fix replaces an NSAssert with a standard if check to handle JSON decoding failures gracefully. Instead of crashing, the method now logs an error and returns nil. Consequently, a nil check has been added to decodeMethodCall to handle this new return case, and the corresponding unit test has been updated to verify the new behavior.

My review includes a suggestion to further improve the robustness of decodeMethodCall by not only checking for nil but also ensuring the decoded object is of the expected NSDictionary type. This will prevent potential crashes from valid but structurally incorrect JSON messages.

Comment on lines 148 to +152
NSDictionary* dictionary = [[FlutterJSONMessageCodec sharedInstance] decode:message];
if (dictionary == nil) {
NSLog(@"Invalid JSON method call. Couldn't decode message: %@", message);
return nil;
}

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

While the added nil check is good, this is also a good opportunity to make this method more robust by checking that the decoded object is actually a dictionary. If decode: returns a valid JSON object that is not a dictionary (e.g., an array or a string), this code could cause a crash when dictionary[@"method"] is accessed.

Using isKindOfClass: handles both the nil case (as [nil isKindOfClass:] returns false) and the incorrect type case in a single check, making the code safer.

Suggested change
NSDictionary* dictionary = [[FlutterJSONMessageCodec sharedInstance] decode:message];
if (dictionary == nil) {
NSLog(@"Invalid JSON method call. Couldn't decode message: %@", message);
return nil;
}
id object = [[FlutterJSONMessageCodec sharedInstance] decode:message];
if (![object isKindOfClass:[NSDictionary class]]) {
NSLog(@"Invalid JSON method call. The decoded message must be a dictionary.");
return nil;
}
NSDictionary* dictionary = (NSDictionary*)object;

@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: 8c9e422.

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:

GitHub Error: You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) If you reach out to GitHub Support for help, please include the request ID 8B5E:1A98A6:B4BC96:30C82BE:693BA1F5.

Stack trace:

#0      GitHub.handleStatusCode (package:github/src/common/github.dart:486:5)
#1      GitHub.request (package:github/src/common/github.dart:422:7)
<asynchronous suspension>
#2      GitHub.requestJson (package:github/src/common/github.dart:323:22)
<asynchronous suspension>
#3      RetryOptions.retry (package:retry/retry.dart:131:16)
<asynchronous suspension>
#4      LuciBuildService.scheduleTryBuilds (package:cocoon_service/src/service/luci_build_service.dart:246:24)
<asynchronous suspension>
#5      Scheduler._runCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1253:9)
<asynchronous suspension>
#6      Scheduler.proceedToCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1314:7)
<asynchronous suspension>
#7      Scheduler._closeSuccessfulEngineBuildStage (package:cocoon_service/src/service/scheduler.dart:1121:5)
<asynchronous suspension>
#8      Scheduler.processCheckRunCompleted (package:cocoon_service/src/service/scheduler.dart:1054:9)
<asynchronous suspension>
#9      PresubmitLuciSubscription.post (package:cocoon_service/src/request_handlers/presubmit_luci_subscription.dart:135:7)
<asynchronous suspension>
#10     RequestHandler.service (package:cocoon_service/src/request_handling/request_handler.dart:45:20)
<asynchronous suspension>
#11     SubscriptionHandler.service (package:cocoon_service/src/request_handling/subscription_handler.dart:140:5)
<asynchronous suspension>
#12     createServer.<anonymous closure> (package:cocoon_service/server.dart:339:7)
<asynchronous suspension>

@chinmaygarde

Copy link
Copy Markdown
Contributor

This changes existing behavior and existing tests verify this. Seems like throwing an exception is WAI.

@mahmuttaskiran

mahmuttaskiran commented Dec 16, 2025

Copy link
Copy Markdown
Contributor Author

This changes existing behavior and existing tests verify this. Seems like throwing an exception is WAI.

Android and iOS behave differently in this case. On Android, the message is encoded using encodeMessage ([StringCodec](https://api.flutter.dev/javadoc/io/flutter/plugin/common/StringCodec.html)), which prevents a crash. I’m not sure if it would be to straightforward to implement the same encoding strategy on iOS, but at a minimum the application shouldn't crash.

Logging the error and continuing without issuing the system call (in this specific case TextInput.setClient) was the only viable approach I could identify. Currently, this crash affects approximately 250 users per month in our application.

While debugging the iOS platform code, I found that the crash is triggered by the following system call:

{"method":"TextInput.setClient","args":[1,{"viewId":0,"inputType":{"name":"TextInputType.text","signed":null,"decimal":null},"readOnly":false,"obscureText":false,"autocorrect":true,"smartDashesType":"1","smartQuotesType":"1","enableSuggestions":true,"enableInteractiveSelection":true,"actionLabel":null,"inputAction":"TextInputAction.done","textCapitalization":"TextCapitalization.none","keyboardAppearance":"Brightness.light","enableIMEPersonalizedLearning":true,"contentCommitMimeTypes":[],"autofill":{"uniqueIdentifier":"EditableText-359188207","hints":[],"editingValue":{"text":"test \udfff test ","selectionBase":1,"selectionExtent":1,"selectionAffinity":"TextAffinity.upstream","selectionIsDirectional":false,"composingBase":-1,"composingExtent":-1}},"enableDeltaModel":false,"hintLocales":null}]}

If you look closely, the "text":"test \udfff test " field contains an invalid Unicode surrogate. This causes the JSON decode operation to fail and ultimately results in a crash due to a nil pointer access.

@loic-sharma

Copy link
Copy Markdown
Member

Hey there, I left some more details on this crash here: #179727 (comment)

Instead of not crashing on invalid Unicode, it seems we should update the iOS embedder to accept Dart's strategy for escaping lone surrogate pairs.

@mahmuttaskiran

Copy link
Copy Markdown
Contributor Author

Hey there, I left some more details on this crash here: #179727 (comment)

Instead of not crashing on invalid Unicode, it seems we should update the iOS embedder to accept Dart's strategy for escaping lone surrogate pairs.

Thank you for the details on the issue. I’ve raised another PR that attempts to resolve it by introducing a sanitizer. I’ve validated that both Android and iOS now behave the same.

- (FlutterMethodCall*)decodeMethodCall:(NSData*)message {
NSDictionary* dictionary = [[FlutterJSONMessageCodec sharedInstance] decode:message];
if (dictionary == nil) {
NSLog(@"Invalid JSON method call. Couldn't decode message: %@", message);

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.

In FluterCodecs.h there's NS_ASSUME_NONNULL_BEGIN so I'm expecting the return type to be non-nullable, I'm a bit surprised that the compiler isn't complaining about this.

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.

The NSLog will be printing out a NSData, not sure what -[NSData description] does, but if it doesn't print out the raw data then it's basically useless, if it does then it may expose sensitive information like PII or passwords in standard output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the review @LongCatIsLooong

In FluterCodecs.h there's NS_ASSUME_NONNULL_BEGIN so I'm expecting the return type to be non-nullable, I'm a bit surprised that the compiler isn't complaining about this.

"-Wnullable-to-nonnull-conversion" is missing in compiler configs for iOS and Mac in
common/confic.gni

After enabling it, compiler gives error for the following files:

../../flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.mm:40:36: error: implicit conversion from nullable pointer 'NSURL * _Nullable' to non-nullable pointer type 'NSURL * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]

AND 

../../flutter/shell/platform/darwin/common/framework/Source/FlutterStandardCodec.mm:49:65: error: implicit conversion from nullable pointer 'NSData *__strong _Nullable' to non-nullable pointer type 'NSData * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]

I'm not sure if it's disabled intentionally. So, I'm leaving it as is

The NSLog will be printing out a NSData, not sure what -[NSData description] does, but if it doesn't print out the raw data then it's basically useless, if it does then it may expose sensitive information like PII or passwords in standard output.

True. Added debug-only preprocessor guard there.

…thub.com:mahmuttaskiran/flutter into fix/flutterjsonmessagecodec-nil-dictionary-crash
@LongCatIsLooong

Copy link
Copy Markdown
Contributor

@mahmuttaskiran do you think we should close this in favor of #179978?

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

Labels

a: desktop Running on desktop 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.

4 participants