This is an automated email from the ASF dual-hosted git repository.

raulcd 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 e611f48081 GH-50707: [C++][Gandiva] fix out-of-bounds read in mask 
utf8proc length (#50709)
e611f48081 is described below

commit e611f48081ec927e18e5bcb72fbd071c15c76b08
Author: Abdul Rawoof Khan <[email protected]>
AuthorDate: Fri Aug 14 13:39:26 2026 +0530

    GH-50707: [C++][Gandiva] fix out-of-bounds read in mask utf8proc length 
(#50709)
    
    ### Rationale for this change
    
    The Gandiva mask stubs walk a utf8 string calling `utf8proc_iterate(data + 
bytes_read, data_len, ...)`, passing the total `data_len` as the remaining byte 
count even though the pointer has already advanced by `bytes_read`. 
`utf8proc_iterate` reads up to `strlen` bytes, so a value ending in a truncated 
multi-byte glyph (for example a lead byte `0xF0` as the final byte of an 
exactly sized value buffer) makes it read continuation bytes past `data + 
data_len`. This is reachable from `mask [...]
    
    ### What changes are included in this PR?
    
    Pass `data_len - bytes_read` (the real remaining length) at all four 
`utf8proc_iterate` call sites so utf8proc reports the truncated glyph instead 
of reading past the buffer. `mask_utf8_utf8_utf8_utf8` also lacked the 
`char_len < 0` check that the other two paths already have, so I added it there 
to reject the now-detected invalid input rather than advancing by a negative 
length.
    
    ### Are these changes tested?
    
    Yes. `TestMaskTruncatedUtf8NoOverread` feeds a value whose reported length 
stops one byte short of a complete euro sign; before the change both functions 
consumed the out-of-range byte and returned a masked result, after it they 
report the truncated input. The existing mask tests still pass.
    
    ### Are there any user-facing changes?
    
    No change for valid utf8. A value that ends in a truncated multi-byte glyph 
now surfaces an invalid-utf8 error instead of being silently masked using bytes 
past its end.
    
    **This PR contains a "Critical Fix".** The wrong length argument lets 
`utf8proc_iterate` read past the end of an exactly sized input buffer.
    
    * GitHub Issue: #50707
    
    Authored-by: abdul rawoof <[email protected]>
    Signed-off-by: Raúl Cumplido <[email protected]>
---
 cpp/src/gandiva/gdv_function_stubs.cc      | 23 +++++++++++++++----
 cpp/src/gandiva/gdv_function_stubs_test.cc | 37 ++++++++++++++++++++++++++++++
 2 files changed, 56 insertions(+), 4 deletions(-)

diff --git a/cpp/src/gandiva/gdv_function_stubs.cc 
b/cpp/src/gandiva/gdv_function_stubs.cc
index 1b359524c2..6b3e9935b0 100644
--- a/cpp/src/gandiva/gdv_function_stubs.cc
+++ b/cpp/src/gandiva/gdv_function_stubs.cc
@@ -489,7 +489,7 @@ const char* gdv_mask_first_n_utf8_int32(int64_t context, 
const char* data,
   while ((chars_masked < n_to_mask) && (bytes_masked < data_len)) {
     auto char_len =
         utf8proc_iterate(reinterpret_cast<const utf8proc_uint8_t*>(data + 
bytes_masked),
-                         data_len, &utf8_char);
+                         data_len - bytes_masked, &utf8_char);
 
     if (char_len < 0) {
       gdv_fn_context_set_error_msg(context, utf8proc_errmsg(char_len));
@@ -597,7 +597,12 @@ const char* gdv_mask_last_n_utf8_int32(int64_t context, 
const char* data,
   while ((bytes_read < data_len) && (chars_counter < (num_of_chars - 
n_to_mask))) {
     auto char_len =
         utf8proc_iterate(reinterpret_cast<const utf8proc_uint8_t*>(data + 
bytes_read),
-                         data_len, &utf8_char);
+                         data_len - bytes_read, &utf8_char);
+    if (char_len < 0) {
+      gdv_fn_context_set_error_msg(context, utf8proc_errmsg(char_len));
+      *out_len = 0;
+      return nullptr;
+    }
     chars_counter++;
     bytes_read += static_cast<int>(char_len);
   }
@@ -611,7 +616,12 @@ const char* gdv_mask_last_n_utf8_int32(int64_t context, 
const char* data,
   while (bytes_read < data_len) {
     auto char_len =
         utf8proc_iterate(reinterpret_cast<const utf8proc_uint8_t*>(data + 
bytes_read),
-                         data_len, &utf8_char);
+                         data_len - bytes_read, &utf8_char);
+    if (char_len < 0) {
+      gdv_fn_context_set_error_msg(context, utf8proc_errmsg(char_len));
+      *out_len = 0;
+      return nullptr;
+    }
     switch (utf8proc_category(utf8_char)) {
       case 1:
         out[out_idx] = 'X';
@@ -700,7 +710,12 @@ const char* mask_utf8_utf8_utf8_utf8(int64_t context, 
const char* data, int32_t
   while (bytes_read < data_len) {
     auto char_len =
         utf8proc_iterate(reinterpret_cast<const utf8proc_uint8_t*>(data + 
bytes_read),
-                         data_len, &utf8_char);
+                         data_len - bytes_read, &utf8_char);
+    if (char_len < 0) {
+      gdv_fn_context_set_error_msg(context, utf8proc_errmsg(char_len));
+      *out_len = 0;
+      return nullptr;
+    }
     switch (utf8proc_category(utf8_char)) {
       case UTF8PROC_CATEGORY_LU:
         memcpy(out + out_index, upper, upper_length);
diff --git a/cpp/src/gandiva/gdv_function_stubs_test.cc 
b/cpp/src/gandiva/gdv_function_stubs_test.cc
index 27ae60694b..c0197eb815 100644
--- a/cpp/src/gandiva/gdv_function_stubs_test.cc
+++ b/cpp/src/gandiva/gdv_function_stubs_test.cc
@@ -1151,6 +1151,43 @@ TEST(TestGdvFnStubs, TestMaskLastN) {
   EXPECT_EQ(expected, std::string(result, out_len));
 }
 
+TEST(TestGdvFnStubs, TestMaskTruncatedUtf8NoOverread) {
+  gandiva::ExecutionContext ctx;
+  int64_t ctx_ptr = reinterpret_cast<int64_t>(&ctx);
+  int32_t out_len = -1;
+
+  // A byte > 127 routes the mask functions through the utf8proc multi-byte 
path.
+  // The buffer holds a complete euro sign (0xE2 0x82 0xAC) but the reported
+  // length stops one byte short, so the trailing glyph is truncated. The
+  // functions must not read the byte past data_len: utf8proc_iterate has to be
+  // told only data_len - offset bytes remain, otherwise it consumes the
+  // out-of-range continuation byte and decodes a full glyph instead of
+  // reporting the truncated input.
+  const char buf[] = {'a', static_cast<char>(0xE2), static_cast<char>(0x82),
+                      static_cast<char>(0xAC)};
+  const int32_t truncated_len = 3;  // 'a' + first two bytes of the euro sign
+
+  ctx.Reset();
+  gdv_mask_first_n_utf8_int32(ctx_ptr, buf, truncated_len, 4, &out_len);
+  EXPECT_EQ(out_len, 0);
+  EXPECT_TRUE(ctx.has_error());
+
+  out_len = -1;
+  ctx.Reset();
+  mask_utf8(ctx_ptr, buf, truncated_len, &out_len);
+  EXPECT_EQ(out_len, 0);
+  EXPECT_TRUE(ctx.has_error());
+
+  // gdv_mask_last_n_utf8_int32 catches the truncated glyph in its
+  // utf8proc_decompose pre-pass, so it reports the invalid input rather than
+  // reading past data_len in the iterate loop.
+  out_len = -1;
+  ctx.Reset();
+  gdv_mask_last_n_utf8_int32(ctx_ptr, buf, truncated_len, 4, &out_len);
+  EXPECT_EQ(out_len, 0);
+  EXPECT_TRUE(ctx.has_error());
+}
+
 TEST(TestGdvFnStubs, TestTranslate) {
   gandiva::ExecutionContext ctx;
   int64_t ctx_ptr = reinterpret_cast<int64_t>(&ctx);

Reply via email to