Repository navigation
[Linux] App aborts with wstring_convert: to_bytes error when typing with an IME — TextInputModel::DeleteSurrounding splits UTF-16 surrogate pairs #190046
Description
Steps to reproduce
Environment: Debian 12 desktop, ibus with a Chinese Wubi (五笔) input method (e.g. ibus-table-wubi) — any IME engine that uses GTK surrounding-text / delete-surrounding should work.
- Run any Flutter Linux desktop app with a
TextField(release or debug — the crash is in the engine). - Make sure the field's text contains an emoji (any non-BMP character), e.g. paste
a😀. - Place the cursor before the emoji and type with the Wubi IME so that the IME issues a
delete-surroundingrequest covering the BMP character and the emoji (Chinese IMEs do this routinely for reconversion / candidate replacement). - The app aborts immediately.
The crash is probabilistic at the UI level (it depends on the IME issuing delete-surrounding across an emoji), but it is deterministic at the unit level — see the minimal reproduction below, which needs no IME at all.
Minimal reproduction (engine unit test)
// engine/src/flutter/shell/platform/common/text_input_model_unittests.cc
TEST(TextInputModel, DeleteSurroundingMustNotSplitSurrogatePairs) {
auto model = std::make_unique<TextInputModel>();
model->SetText("a😀"); // UTF-16: 'a', 0xD83D, 0xDE00
EXPECT_TRUE(model->SetSelection(TextRange(0)));
model->DeleteSurrounding(0, 2); // delete 'a' and the emoji
EXPECT_EQ(model->GetText(), ""); // ABORTS before the comparison
}Trace of DeleteSurrounding(0, 2) with text_ = {'a', 0xD83D, 0xDE00}, start = 0:
- iteration 0:
end += IsLeadingSurrogate(text_.at(start /* =0, 'a' */)) ? 2 : 1→end = 1 - iteration 1:
end += IsLeadingSurrogate(text_.at(start /* still 0, 'a' */)) ? 2 : 1→end = 2
(should have checkedtext_.at(end /* =1, 0xD83D */)and advanced by 2) text_.erase(0, 2)removes'a'and the lead surrogate only, leaving a lone trail surrogate0xDE00intext_.
The next GetText() (called unconditionally at the top of im_delete_surrounding_cb, and by update_editing_state) runs fml::Utf16ToUtf8 → std::wstring_convert::to_bytes, which throws std::range_error on the lone surrogate. The engine is built with -fno-exceptions, so this is an immediate abort:
range_error was thrown in -fno-exceptions mode with message "wstring_convert: to_bytes error"
Aborted
Root cause
TextInputModel::DeleteSurrounding advances end by checking the character at start instead of at end:
flutter/engine/src/flutter/shell/platform/common/text_input_model.cc
Lines 207 to 210 in 058e0af
| auto end = start; | |
| for (int i = 0; i < count && end != max_pos; i++) { | |
| end += IsLeadingSurrogate(text_.at(start)) ? 2 : 1; | |
| } |
auto end = start;
for (int i = 0; i < count && end != max_pos; i++) {
end += IsLeadingSurrogate(text_.at(start)) ? 2 : 1; // <-- should be text_.at(end)
}Consequences:
- first deleted char is BMP, a later one is non-BMP →
endlands mid-surrogate-pair,eraseleaves a lone surrogate intext_, and the next UTF-16→UTF-8 conversion aborts the process; - first deleted char is non-BMP, later ones are BMP →
endover-advances and deletes more text than requested (silent data corruption).
Crash entry point on Linux (GTK delete-surrounding, used heavily by Chinese/Japanese IMEs over ibus/fcitx):
flutter/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc
Lines 226 to 241 in 058e0af
| static gboolean im_delete_surrounding_cb(FlTextInputHandler* self, | |
| gint offset, | |
| gint n_chars) { | |
| std::string text_before_change = self->text_model->GetText(); | |
| if (self->text_model->DeleteSurrounding(offset, n_chars)) { | |
| if (self->enable_delta_model) { | |
| flutter::TextEditingDelta delta = flutter::TextEditingDelta( | |
| text_before_change, self->text_model->composing_range(), | |
| self->text_model->GetText()); | |
| update_editing_state_with_delta(self, &delta); | |
| } else { | |
| update_editing_state(self); | |
| } | |
| } | |
| return TRUE; | |
| } |
Note that DeleteSurrounding is in shell/platform/common, so Windows is affected by the same defect as well (Windows reaches it through its own text input plugin paths).
Related hardening opportunity
TextInputModel::GetCursorOffset is a second unguarded conversion site: if selection_.extent() ever lands inside a surrogate pair (e.g. via an inconsistent preedit cursor offset from im_preedit_changed_cb), substr produces a string ending in a lone lead surrogate and fml::Utf16ToUtf8 aborts the same way:
flutter/engine/src/flutter/shell/platform/common/text_input_model.cc
Lines 301 to 306 in 058e0af
| int TextInputModel::GetCursorOffset() const { | |
| // Measure the length of the current text up to the selection extent. | |
| // There is probably a much more efficient way of doing this. | |
| auto leading_text = text_.substr(0, selection_.extent()); | |
| return fml::Utf16ToUtf8(leading_text).size(); | |
| } |
Since fml::Utf16ToUtf8 / Utf8ToUtf16 (std::wstring_convert, deprecated since C++17) turn any invalid input into a process abort under -fno-exceptions, it may be worth making the conversion lenient (replacement character) in addition to fixing the call sites. This is the same failure signature as #154072 and the macOS issue #182443.
Expected results
Deleting surrounding text that contains non-BMP characters deletes whole code points; the app does not crash.
Actual results
The app aborts with:
range_error was thrown in -fno-exceptions mode with message "wstring_convert: to_bytes error"
Aborted (core dumped)
Suggested fix
auto end = start;
for (int i = 0; i < count && end != max_pos; i++) {
end += IsLeadingSurrogate(text_.at(end)) ? 2 : 1;
}Flutter version
Flutter 3.44.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 058e0af2c2 • 2026-07-23 10:56:21 -0700
Engine • hash 13ffd72b2f9a5ca4db2a74ea52d5353ec2e8f939 (revision 0cd610717b) • 2026-07-23 16:11:34.000Z
Tools • Dart 3.12.2 • DevTools 2.57.0
Reproduced on Debian 12 (GNOME/X11, ibus). The defect is present on current master — text_input_model.cc has not been modified since the code was introduced.
Activity
mbcorona commentedon Jul 28, 2026
HI @chen00054, thanks for the detailed report and for pointing to the source code.
Routing to the Linux team for evaluation.
gaaclarke commentedon Aug 3, 2026
@mbcorona I think it's generally better to let downstream engineers use the
fyi-enginelabel as a way for engineers to tag in other teams if they need help.Vi-debug commentedon Aug 4, 2026
Opened PR #190514 resolve this issue
Fix DeleteSurrounding splitting UTF-16 surrogate pairs
Fix DeleteSurrounding splitting UTF-16 surrogate pairs
Fix DeleteSurrounding splitting UTF-16 surrogate pairs
robert-ancell commentedon Aug 19, 2026
While filed against Linux, the fix suggests this could affect multiple platforms.
Fix DeleteSurrounding splitting UTF-16 surrogate pairs (flutter#190514)
github-actions commentedon Sep 17, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.