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(

Reply via email to