This is an automated email from the ASF dual-hosted git repository.
kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 1f3f40f164 GH-50462: [C++][Gandiva] fix out-of-bounds read in
translate_utf8_utf8_utf8 (#50463)
1f3f40f164 is described below
commit 1f3f40f16461d3d0945bdff1f0a1e674fde9bd9a
Author: Abdul Rawoof Khan <[email protected]>
AuthorDate: Thu Jul 16 07:06:53 2026 +0530
GH-50462: [C++][Gandiva] fix out-of-bounds read in translate_utf8_utf8_utf8
(#50463)
### Rationale for this change
The multi-byte branch of `translate_utf8_utf8_utf8` (taken whenever the
input, FROM, or TO contains a byte > 127) reads `gdv_fn_utf8_char_length` on a
lead byte and then builds a `std::string` of that width from the buffer without
checking how many bytes remain. A truncated trailing glyph in the input reads
past IN, a multi-byte input character missing from FROM reads one byte past
FROM at the end-of-list sentinel (the `from_for == from_len` check ran after
the read), and a truncated [...]
### What changes are included in this PR?
Clamp each utf8 character width to the bytes left in its buffer before
copying, consuming a single byte on a truncated or invalid lead byte (which
also stops a zero-width advance from looping forever), and move the FROM
end-of-list check ahead of the read so the sentinel iteration no longer touches
`from[from_len]`. Valid input keeps its existing grouping since the clamp only
fires past the end.
### Are these changes tested?
Yes. Added two cases to `TestGdvFnStubs.TestTranslate`: a truncated
trailing glyph in the input, and a valid multi-byte input char absent from
FROM. Both use exact-sized buffers so ASAN trips on the pre-patch over-read and
passes after the fix.
### Are there any user-facing changes?
No.
**This PR contains a "Critical Fix".** It fixes a heap out-of-bounds read
(crash) in `translate` reachable from user-supplied string data.
* GitHub Issue: #50462
Authored-by: abdul rawoof <[email protected]>
Signed-off-by: Sutou Kouhei <[email protected]>
---
cpp/src/gandiva/gdv_function_stubs_test.cc | 23 +++++++++++++++++++++++
cpp/src/gandiva/gdv_string_function_stubs.cc | 22 ++++++++++++++++++----
2 files changed, 41 insertions(+), 4 deletions(-)
diff --git a/cpp/src/gandiva/gdv_function_stubs_test.cc
b/cpp/src/gandiva/gdv_function_stubs_test.cc
index 3067a5f275..27ae60694b 100644
--- a/cpp/src/gandiva/gdv_function_stubs_test.cc
+++ b/cpp/src/gandiva/gdv_function_stubs_test.cc
@@ -1202,6 +1202,29 @@ TEST(TestGdvFnStubs, TestTranslate) {
EXPECT_STREQ(result, "");
EXPECT_THAT(ctx.get_error(),
::testing::HasSubstr("Would overflow maximum output size"));
+
+ // A byte > 127 selects the multi-byte path. A truncated trailing glyph (0xE2
+ // claims a 3-byte character but only one byte is present) must not be read
past
+ // the end of the input; it is passed through as a single byte. Exact-sized
+ // buffers let ASAN catch any over-read here.
+ const char truncated_in[] = {'a', static_cast<char>(0xE2)};
+ result = translate_utf8_utf8_utf8(ctx_ptr, truncated_in, 2, "x", 1, "y", 1,
&out_len);
+ EXPECT_EQ(std::string(truncated_in, 2), std::string(result, out_len));
+
+ // A valid multi-byte input character absent from FROM previously over-read
FROM
+ // by one byte at the end-of-list sentinel.
+ const char euro[] = {static_cast<char>(0xE2), static_cast<char>(0x82),
+ static_cast<char>(0xAC)};
+ const char from_one[] = {'a'};
+ const char to_one[] = {'b'};
+ result = translate_utf8_utf8_utf8(ctx_ptr, euro, 3, from_one, 1, to_one, 1,
&out_len);
+ EXPECT_EQ(std::string(euro, 3), std::string(result, out_len));
+
+ // A truncated trailing glyph in TO (0xE2 claims a 3-byte character but only
one
+ // byte is present) previously over-read TO when a matched input char mapped
to it.
+ const char trunc_to[] = {static_cast<char>(0xE2)};
+ result = translate_utf8_utf8_utf8(ctx_ptr, "a", 1, "a", 1, trunc_to, 1,
&out_len);
+ EXPECT_EQ(std::string(trunc_to, 1), std::string(result, out_len));
}
TEST(TestGdvFnStubs, TestToUtcTimezone) {
diff --git a/cpp/src/gandiva/gdv_string_function_stubs.cc
b/cpp/src/gandiva/gdv_string_function_stubs.cc
index b3159a2d74..a0936d448a 100644
--- a/cpp/src/gandiva/gdv_string_function_stubs.cc
+++ b/cpp/src/gandiva/gdv_string_function_stubs.cc
@@ -779,6 +779,11 @@ const char* translate_utf8_utf8_utf8(int64_t context,
const char* in, int32_t in
for (int32_t in_for = 0; in_for < in_len; in_for += len_char_in) {
// Updating len to char in this position
len_char_in = gdv_fn_utf8_char_length(in[in_for]);
+ // A truncated or invalid lead byte at the tail would make the copy
below read
+ // past IN, and a zero width would spin the loop forever; consume one
byte.
+ if (len_char_in == 0 || in_for + len_char_in > in_len) {
+ len_char_in = 1;
+ }
// Making copy to std::string with length for this char position
std::string insert_copy_key(in + in_for, len_char_in);
if (subs_list.find(insert_copy_key) != subs_list.end()) {
@@ -790,10 +795,6 @@ const char* translate_utf8_utf8_utf8(int64_t context,
const char* in, int32_t in
}
} else {
for (int32_t from_for = 0; from_for <= from_len; from_for +=
len_char_from) {
- // Updating len to char in this position
- len_char_from = gdv_fn_utf8_char_length(from[from_for]);
- // Making copy to std::string with length for this char position
- std::string copy_from_compare(from + from_for, len_char_from);
if (from_for == from_len) {
// If it's not in the FROM list, just add it to the map and the
result.
std::string insert_copy_value(in + in_for, len_char_in);
@@ -806,6 +807,15 @@ const char* translate_utf8_utf8_utf8(int64_t context,
const char* in, int32_t in
break;
}
+ // Updating len to char in this position
+ len_char_from = gdv_fn_utf8_char_length(from[from_for]);
+ // Clamp a truncated or invalid lead byte to the remaining bytes so
the copy
+ // below never reads past FROM and the loop always advances.
+ if (len_char_from == 0 || from_for + len_char_from > from_len) {
+ len_char_from = 1;
+ }
+ // Making copy to std::string with length for this char position
+ std::string copy_from_compare(from + from_for, len_char_from);
if (insert_copy_key != copy_from_compare) {
// If this character does not exist in FROM list, don't need
treatment
continue;
@@ -818,6 +828,10 @@ const char* translate_utf8_utf8_utf8(int64_t context,
const char* in, int32_t in
// If exist and the start_compare is in range, add to map with the
// corresponding TO in position start_compare
len_char_to = gdv_fn_utf8_char_length(to[start_compare]);
+ // Clamp a truncated or invalid lead byte to the remaining bytes
of TO.
+ if (len_char_to == 0 || start_compare + len_char_to > to_len) {
+ len_char_to = 1;
+ }
std::string insert_copy_value(to + start_compare, len_char_to);
// Insert in map to next loops
subs_list.insert(