Memory safety / out-of-bounds access

HIGH
flutter/flutter
Commit: 1e36b1c023ef
Affected: <= 1.16.2
2026-06-18 04:32 UTC

Description

The commit fixes a memory-safety vulnerability in the Linux FlAccessibleTextField implementation by adding explicit bounds checks when computing substrings and when deriving string bounds from ATK client requests. Specifically: - get_substring now clamps start and end to [0, length] and enforces start <= end before calling g_utf8_substring, preventing out-of-bounds memory access if an ATK client passes offsets beyond the text length. - get_string_at_offset clamps start and end to [0, max(n_attrs - 1, 0)] before accessing the PangoLogAttr array, mitigating potential out-of-bounds access when ATK/granularity requests refer to attributes beyond the available attributes. The patch also adds tests covering offset-beyond-end, empty text, and offset-at-end boundary scenarios. This is a genuine memory-safety vulnerability fix rather than a pure dependency bump or cosmetic cleanup. In short, external ATK clients that query text with out-of-range offsets could previously trigger undefined behavior or memory access violations; the fix ensures all indexing is bounded and safe.

Proof of Concept

PoC (conceptual, reproduce-then-validate): 1) Build a small ATK-enabled GTK application that uses Flutter's Linux accessibility implementation and creates a FlAccessibleTextField with the value "Hello". 2) As an ATK client, request text ranges/offsets beyond the actual text length: - atk_text_get_text(ATK_TEXT(node), 100, -1) // start well beyond end - atk_text_get_string_at_offset(ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset) // offset far beyond end - atk_text_get_string_at_offset(ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset) 3) Observe behavior before the fix vs after: - Pre-fix: likely memory safety violation (crash/segfault, or corrupted memory) due to out-of-bounds access in either the substring code path or the PangoLogAttr array access. - Post-fix (as implemented): the calls return safely with start_offset/end_offset clamped to valid bounds and returned strings either as empty or within the text, without crashing. Note: The repository includes test coverage (GetTextBoundsChecking and several get_string_at_offset boundary tests) that encode the same scenarios intended to validate the fix.

Commit Details

Author: Robert Ancell

Date: 2026-06-18 03:14 UTC

Message:

Fix bounds checking in FlAccessibleTextField (#188137) Add bounds validation to get_substring and get_string_at_offset to prevent out-of-bounds memory access when ATK clients pass offsets beyond the text length. - get_substring: clamp start and end to [0, length] and ensure start <= end before calling g_utf8_substring. - get_string_at_offset: clamp start and end to [0, n_attrs-1] before accessing the PangoLogAttr array. Add tests for offset-beyond-end, empty text, and offset-at-end boundary conditions.

Triage Assessment

Vulnerability Type: Memory safety (out-of-bounds access)

Confidence: HIGH

Reasoning:

The patch adds bounds clamping when computing substrings and retrieving string attributes, preventing out-of-bounds memory access when external clients (ATK) pass offsets beyond text length. This is a memory safety vulnerability fix (bounds checking).

Verification Assessment

Vulnerability Type: Memory safety / out-of-bounds access

Confidence: HIGH

Affected Versions: <= 1.16.2

Code Diff

diff --git a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc index 17d0a0d763dc4..ce8ace383a161 100644 --- a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc +++ b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc @@ -39,10 +39,13 @@ static gchar* get_substring(FlAccessibleTextField* self, glong start, glong end) { const gchar* value = gtk_entry_buffer_get_text(self->buffer); + glong length = g_utf8_strlen(value, -1); if (end == -1) { // g_utf8_substring() accepts -1 since 2.72 - end = g_utf8_strlen(value, -1); + end = length; } + start = CLAMP(start, 0, length); + end = CLAMP(end, start, length); return g_utf8_substring(value, start, end); } @@ -76,16 +79,20 @@ static gchar* get_string_at_offset(FlAccessibleTextField* self, const PangoLogAttr* attrs = pango_layout_get_log_attrs_readonly(layout, &n_attrs); + start = CLAMP(start, 0, MAX(n_attrs - 1, 0)); + end = CLAMP(end, 0, MAX(n_attrs - 1, 0)); + while (start > 0 && !is_start(&attrs[start])) { --start; } - if (start_offset != nullptr) { - *start_offset = start; - } while (end < n_attrs && !is_end(&attrs[end])) { ++end; } + + if (start_offset != nullptr) { + *start_offset = start; + } if (end_offset != nullptr) { *end_offset = end; } diff --git a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc index 90be661eb82c7..2cbdb890609ae 100644 --- a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc @@ -218,6 +218,33 @@ TEST(FlAccessibleTextFieldTest, GetText) { EXPECT_STREQ(tt, "tt"); } +// Tests AtkText::get_text with out-of-bounds offsets. +TEST(FlAccessibleTextFieldTest, GetTextBoundsChecking) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello"); + + // start beyond end of text + g_autofree gchar* beyond = atk_text_get_text(ATK_TEXT(node), 100, -1); + EXPECT_STREQ(beyond, ""); + + // end beyond text length + g_autofree gchar* end_beyond = atk_text_get_text(ATK_TEXT(node), 2, 100); + EXPECT_STREQ(end_beyond, "llo"); + + // both beyond text length + g_autofree gchar* both_beyond = atk_text_get_text(ATK_TEXT(node), 50, 100); + EXPECT_STREQ(both_beyond, ""); + + // empty buffer + fl_accessible_node_set_value(node, ""); + g_autofree gchar* empty_beyond = atk_text_get_text(ATK_TEXT(node), 5, 10); + EXPECT_STREQ(empty_beyond, ""); +} + // Tests AtkText::get_caret_offset. TEST(FlAccessibleTextFieldTest, GetCaretOffset) { g_autoptr(FlDartProject) project = fl_dart_project_new(); @@ -696,4 +723,115 @@ TEST(FlAccessibleTextFieldTest, TextBoundary) { EXPECT_EQ(end_offset, 70); } +// Tests that get_string_at_offset handles offset beyond text length. +TEST(FlAccessibleTextFieldTest, TextBoundaryOffsetBeyondEnd) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello"); + + // Offset well beyond the text length should not crash and should return + // a valid result clamped to the text boundaries. + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_CHAR, &start_offset, + &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, 5); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_WORD, &start_offset, + &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, 5); + + g_autofree gchar* sentence_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_SENTENCE, &start_offset, + &end_offset); + EXPECT_NE(sentence_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, 5); +} + +// Tests that get_string_at_offset handles offset at position zero. +TEST(FlAccessibleTextFieldTest, TextBoundaryOffsetAtStart) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello"); + + // Offset at zero should return the first character/word. + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_EQ(end_offset, 1); + EXPECT_STREQ(char_result, "H"); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_EQ(end_offset, 5); + EXPECT_STREQ(word_result, "Hello"); +} + +// Tests that get_string_at_offset handles empty text. +TEST(FlAccessibleTextFieldTest, TextBoundaryEmptyText) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + // Empty text - should not crash. + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_STREQ(char_result, ""); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_STREQ(word_result, ""); +} + +// Tests that get_string_at_offset handles offset at exact text length. +TEST(FlAccessibleTextFieldTest, TextBoundaryOffsetAtEnd) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello world"); + + // Offset at exactly the character count (one past last char). + gint char_count = atk_text_get_character_count(ATK_TEXT(node)); + EXPECT_EQ(char_count, 11); + + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), char_count, ATK_TEXT_GRANULARITY_CHAR, &start_offset, + &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, char_count); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), char_count, ATK_TEXT_GRANULARITY_WORD, &start_offset, + &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, char_count); +} + // NOLINTEND(clang-analyzer-core.StackAddressEscape)
← Back to Alerts View on GitHub →