Repository navigation
Invalid Unicode in TextEditingController's text Crashes the App on iOS #179727
Copy link
Copy link
Open
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work lista: error messageError messages from the Flutter frameworkError messages from the Flutter frameworka: text inputEntering text in a text field or keyboard related problemsEntering text in a text field or keyboard related problemsc: crashStack traces logged to the consoleStack traces logged to the consolec: fatal crashCrashes that terminate the processCrashes that terminate the processengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.38Found to occur in 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40Found to occur in 3.40has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesmaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyplatform-macosBuilding on or for macOS specificallyBuilding on or for macOS specificallyteam-iosOwned by iOS platform teamOwned by iOS platform teamtriaged-iosTriaged by iOS platform teamTriaged by iOS platform team
Description
Steps to reproduce
App crashes on iOS when a TextField contains malformed Unicode (e.g., half of a surrogate pair such as \u{DFFF}).
Expected results
No crash
Actual results
Crash
Code sample
Code sample
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
TextField(
controller: TextEditingController(text: '\u{DFFF}'),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Input Text',
helperText: 'Value: \\u{DFFF}',
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
Fatal Exception: NSInternalInconsistencyException
0 CoreFoundation 0xc98c8 __exceptionPreprocess
1 libobjc.A.dylib 0x317c4 objc_exception_throw
2 Foundation 0x8f3e80 _userInfoForFileAndLine
3 Flutter 0x4dcfd0 -[FlutterJSONMessageCodec decode:] + 111 (FlutterCodecs.mm:111)
4 Flutter 0x4dc42c -[FlutterJSONMethodCodec decodeMethodCall:] + 145 (FlutterCodecs.mm:145)
5 Flutter 0x4d9208 __45-[FlutterMethodChannel setMethodCallHandler:]_block_invoke + 312 (FlutterChannels.mm:312)
6 Flutter 0x6a8e0 invocation function for block in flutter::PlatformMessageHandlerIos::HandlePlatformMessage(std::_fl::unique_ptr<flutter::PlatformMessage, std::_fl::default_delete<flutter::PlatformMessage>>) + 110 (ref_ptr.h:110)
7 libdispatch.dylib 0x1adc _dispatch_call_block_and_release
8 libdispatch.dylib 0x1b7ec _dispatch_client_callout
9 libdispatch.dylib 0x38b24 _dispatch_main_queue_drain.cold.5
10 libdispatch.dylib 0x10ec8 _dispatch_main_queue_drain
11 libdispatch.dylib 0x10e04 _dispatch_main_queue_callback_4CF
12 CoreFoundation 0x6b520 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
13 CoreFoundation 0x1dd14 __CFRunLoopRun
14 CoreFoundation 0x1cc44 _CFRunLoopRunSpecificWithOptions
15 GraphicsServices 0x1498 GSEventRunModal
16 UIKitCore 0xa9dcc (Missing UUID 17c42b4970d63ee2aac77010bb917cdc)
17 UIKitCore 0x4eb0c (Missing UUID 17c42b4970d63ee2aac77010bb917cdc)
18 UIKitCore 0x18a854 (Missing UUID 17c42b4970d63ee2aac77010bb917cdc)
19 Runner 0x2f344 main (StandaloneAppDelegate.swift)
20 ??? 0x19a0aae28 (Missing)
Flutter Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.38.3, on macOS 15.6.1 24G90 darwin-arm64, locale en-AE)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
[!] Xcode - develop for iOS and macOS (Xcode 26.1.1)
✗ CocoaPods not installed.
CocoaPods is a package manager for iOS or macOS platform code.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/to/platform-plugins
For installation instructions, see https://guides.cocoapods.org/using/getting-started.html#installation
[✓] Chrome - develop for the web
[✓] Connected device (3 available)
[✓] Network resources
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work lista: error messageError messages from the Flutter frameworkError messages from the Flutter frameworka: text inputEntering text in a text field or keyboard related problemsEntering text in a text field or keyboard related problemsc: crashStack traces logged to the consoleStack traces logged to the consolec: fatal crashCrashes that terminate the processCrashes that terminate the processengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.38Found to occur in 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40Found to occur in 3.40has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesmaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyplatform-macosBuilding on or for macOS specificallyBuilding on or for macOS specificallyteam-iosOwned by iOS platform teamOwned by iOS platform teamtriaged-iosTriaged by iOS platform teamTriaged by iOS platform team
Activity
[-]Invalid Unicode in TextEditingController's `text` Crashes the App[/-][+]Invalid Unicode in TextEditingController's `text` Crashes the App on iOS[/+]tirth-patel-nc commentedon Dec 11, 2025
Thanks for the report. Seeing the same behaviour with latest SDK versions on iOS & macOS.
It doesn't crash on Android/Web but contains some error logs in the console.
Full Crash - iOS
iOS - Console
Web - Console
Could not find a set of Noto fonts to display all missing characters. Please add a font asset for the missing characters. See: https://docs.flutter.dev/cookbook/design/fontsAndroid - Console
#138464 might be related.
1 remaining item
loic-sharma commentedon Dec 11, 2025
This seems to be an Objective-C crash, I suspect we should redirect this to the iOS team. I'll investigate and reroute if necessary.
loic-sharma commentedon Dec 17, 2025
In this scenario, the framework encodes a
TextInput.setEditingStateJSON message with atextvalue that contains a lone surrogate. Dart's JSON encoder escapes the lone surrogate, but iOS'sNSJSONSerializationis unable to decode this escaped lone surrogate.Here's what the encoded JSON looks like:
{"method":"TextInput.setEditingState","args":{"text":"\udfff","selectionBase":1,"selectionExtent":1,"selectionAffinity":"TextAffinity.upstream","selectionIsDirectional":false,"composingBase":-1,"composingExtent":-1}}It seems that iOS's JSON decoder will need to be fixed to support decoding Dart's pattern for escaping lone surrogates:
flutter/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm
Lines 86 to 113 in 63cfdfa
You can reproduce this problem by adding the following unit test to
shell/platform/darwin/common/framework/Source/flutter_codecs_unittest.mm:I'll review my findings with my co-workers in this week's text input triage. If they agree the fix should be in the iOS embedder, I'll redirect this issue to the iOS team. cc @LongCatIsLooong.
I'll downgrade this from a P1 to a P2 since this does not appear to be a recent regression.
loic-sharma commentedon Dec 18, 2025
Redirecting this to the iOS team. It seems the iOS embedder will need to be updated to be able to decode JSON strings that Dart escaped due to lone surrogates. It's possible the iOS embedder might have additional issues with these malformed strings even after we fix this JSON decoding..
LongCatIsLooong commentedon Jan 2, 2026
I'm leaning towards the framework asserting when you get a malformed string instead of trying to handle unpaired surrogates (the framework is already doing that in some places, e.g.,
flutter/packages/flutter/lib/src/painting/text_painter.dart
Line 190 in c00f050
I think it should be up to the app developer to sanitize the user input, IIRC it's almost never desirable/intentional to have unpaired surrogates in your string (unless you're trying to break something).
I agree that the crash shouldn't happen in production but there are so many services (for example the input method) that are going to be consuming the string and it's very difficult to guarantee that all of them can handle malformed strings so from that perspective we should fail faster.
Also the JSON spec says the unpaired surrogate can be escaped but it's not required. I suspect the GTK and windows implementation will have the same problem to some degree, IIRC they use UTF8.
mahmuttaskiran commentedon Jan 5, 2026
Thank you @LongCatIsLooong for the review and your input.
We’ve fixed the issue on our side by implementing a sanitizer in Dart and applying it as a TextField formatter. However, I believe this issue should also be addressed at the engine level, as the app should not crash in production.
We observed crashes in some user flows caused by a system channel call (JSON with a malformed string). We were unable to reproduce the issue as an end user, but we were able to reproduce it in code by manually providing a malformed string (half of a surrogate pair).
This suggests that other system channel calls may also receive malformed strings and potentially crash the app. In practice, we may never know the exact source of such input—it could come from a speech-to-text service, a custom keyboard, or another external provider.
As you mentioned, many services consume strings, and it’s difficult to guarantee that malformed input will never occur.
On Android, system channel calls are sanitized, so no crash occurs.
On iOS and macOS, the app crashes.
I haven’t been able to test this on Windows/Linux platforms yet.
I see that you reviewed #179775, it basically avoids crash by returning nil and skipping the system call. It might not be the optimal solution as it's discussed in the comments.
But there is another PR (#179978) introducing a sanitizer to fix the problem. Could you please review it and share your ideas on that?
LongCatIsLooong commentedon Jan 8, 2026
Yeah the approach makes sense, however when unpaired surrogates are discarded and replaced with the replacement character in text fields, the framework will likely receive an update telling it that the string in the text field has changed, and the framework would usually invoke user callbacks like
TextField.onChangedassuming the change is user input originated from the IME.This IMO is acceptable but not ideal, and in that case I think it's more important to let developers know that if you have malformed strings in a text field, it may not crash in production but the behavior is not defined so all bets are off.