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 39125bac39 GH-50355: [C++][Gandiva] fix out-of-bounds read in 
utf8_length_ignore_invalid (#50356)
39125bac39 is described below

commit 39125bac3960fb0a0625d924b5b3fe174974f530
Author: Abdul Rawoof Khan <[email protected]>
AuthorDate: Wed Jul 8 14:06:13 2026 +0530

    GH-50355: [C++][Gandiva] fix out-of-bounds read in 
utf8_length_ignore_invalid (#50356)
    
    ### Rationale for this change
    
    `utf8_length_ignore_invalid` extends `char_len` while scanning the bytes 
after a lead byte and never rechecks the buffer end, so an input ending in a 
truncated multi-byte utf8 sequence (a `0xF0` lead byte followed by 
non-continuation bytes) reads past `data_len`. It is reached from untrusted 
string data through `lpad`/`rpad`, which count the input glyphs before padding. 
Reproduced against a verbatim copy of the function under AddressSanitizer with 
the 4-byte input `{0xF0, 'a', 'a', 'a [...]
    
    ### What changes are included in this PR?
    
    Stop the inner scan with a `break` when a byte after the lead byte is not a 
continuation byte, instead of incrementing `char_len`. Growing `char_len` on 
each stray byte kept extending the loop past `data_len`; breaking leaves 
`char_len` bounded so the outer `i + char_len <= data_len` check keeps every 
read in range. Valid input counts the same, because a well-formed glyph has 
only continuation bytes after its lead byte and never hits the `break`.
    
    ### Are these changes tested?
    
    Yes. Added `TestStringOps.TestPadMalformedUtf8NoOverread`, which runs 
`lpad`/`rpad` on the truncated multi-byte input placed in an exactly-sized heap 
buffer so the over-read trips ASAN, and asserts the full padded output. The 
existing pad tests still pass.
    
    ### Are there any user-facing changes?
    
    No.
    
    **This PR contains a "Critical Fix".** It fixes an out-of-bounds read in 
the Gandiva utf8 length helper reachable from `lpad`/`rpad` on crafted string 
data.
    
    * GitHub Issue: #50355
    
    Authored-by: abdul rawoof <[email protected]>
    Signed-off-by: Sutou Kouhei <[email protected]>
---
 cpp/src/gandiva/precompiled/string_ops.cc      |  7 +++-
 cpp/src/gandiva/precompiled/string_ops_test.cc | 56 ++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 1 deletion(-)

diff --git a/cpp/src/gandiva/precompiled/string_ops.cc 
b/cpp/src/gandiva/precompiled/string_ops.cc
index 70cad7c626..9d771758bb 100644
--- a/cpp/src/gandiva/precompiled/string_ops.cc
+++ b/cpp/src/gandiva/precompiled/string_ops.cc
@@ -208,7 +208,12 @@ gdv_int32 utf8_length_ignore_invalid(const char* data, 
gdv_int32 data_len) {
     }
     for (int j = 1; j < char_len; ++j) {
       if ((data[i + j] & 0xC0) != 0x80) {  // bytes following head-byte of 
glyph
-        char_len += 1;
+        // Only the bytes up to the mismatch belong to this (invalid) glyph, so
+        // advance past them and let the outer loop re-parse the rest. Keeping
+        // char_len at its declared width would swallow valid characters that
+        // fall inside the truncated sequence's window.
+        char_len = j;
+        break;
       }
     }
     ++count;
diff --git a/cpp/src/gandiva/precompiled/string_ops_test.cc 
b/cpp/src/gandiva/precompiled/string_ops_test.cc
index c866b88540..3a16e4076b 100644
--- a/cpp/src/gandiva/precompiled/string_ops_test.cc
+++ b/cpp/src/gandiva/precompiled/string_ops_test.cc
@@ -18,7 +18,9 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
+#include <cstring>
 #include <limits>
+#include <memory>
 
 #include "gandiva/execution_context.h"
 #include "gandiva/precompiled/types.h"
@@ -1608,6 +1610,60 @@ TEST(TestStringOps, TestRpadString) {
   EXPECT_EQ(std::string(out_str + 5000, 2), "α");
 }
 
+TEST(TestStringOps, TestPadMalformedUtf8NoOverread) {
+  gandiva::ExecutionContext ctx;
+  uint64_t ctx_ptr = reinterpret_cast<gdv_int64>(&ctx);
+  gdv_int32 out_len = 0;
+
+  // A 4-byte utf8 lead byte followed by non-continuation bytes and no trailing
+  // space. utf8_length_ignore_invalid() used to extend the glyph length past
+  // the end of the buffer while scanning the continuation bytes. The input is
+  // held in an exactly-sized heap buffer so any over-read trips 
AddressSanitizer.
+  const char bytes[] = {'\xF0', 'a', 'a', 'a'};
+  const auto text_len = static_cast<gdv_int32>(sizeof(bytes));
+  std::unique_ptr<char[]> text(new char[text_len]);
+  std::memcpy(text.get(), bytes, text_len);
+  const std::string text_str(text.get(), text_len);
+
+  // The lone lead byte counts as one invalid glyph and the three 'a's as one
+  // each, so the length is 4 and padding to width 6 adds two fill characters.
+  const char* out_str =
+      lpad_utf8_int32_utf8(ctx_ptr, text.get(), text_len, 6, " ", 1, &out_len);
+  EXPECT_EQ(out_len, 6);
+  EXPECT_EQ(std::string(out_str, out_len), "  " + text_str);
+
+  out_str = rpad_utf8_int32_utf8(ctx_ptr, text.get(), text_len, 6, " ", 1, 
&out_len);
+  EXPECT_EQ(out_len, 6);
+  EXPECT_EQ(std::string(out_str, out_len), text_str + "  ");
+}
+
+TEST(TestStringOps, TestPadMalformedUtf8KeepsValidGlyph) {
+  gandiva::ExecutionContext ctx;
+  uint64_t ctx_ptr = reinterpret_cast<gdv_int64>(&ctx);
+  gdv_int32 out_len = 0;
+
+  // {0xF0, 'a', 0xE2, 0x82, 0xAC}: malformed 4-byte lead + ASCII 'a' + U+20AC 
€.
+  // 0xF0 alone counts as one invalid glyph, then 'a' and € follow on their 
own,
+  // so the count is 3. If the inner scan kept char_len at 4 it would advance 
the
+  // outer loop past 'a', 0xE2, 0x82 and only see the orphaned 0xAC, giving 2.
+  // The input sits in an exactly-sized heap buffer so any over-read trips 
ASAN.
+  const char bytes[] = {'\xF0', 'a', '\xE2', '\x82', '\xAC'};
+  const auto text_len = static_cast<gdv_int32>(sizeof(bytes));
+  std::unique_ptr<char[]> text(new char[text_len]);
+  std::memcpy(text.get(), bytes, text_len);
+  const std::string text_str(text.get(), text_len);
+
+  // 3 glyphs padded to width 5 adds two fill characters, out_len = 2 + 5 = 7.
+  const char* out_str =
+      lpad_utf8_int32_utf8(ctx_ptr, text.get(), text_len, 5, " ", 1, &out_len);
+  EXPECT_EQ(out_len, 7);
+  EXPECT_EQ(std::string(out_str, out_len), "  " + text_str);
+
+  out_str = rpad_utf8_int32_utf8(ctx_ptr, text.get(), text_len, 5, " ", 1, 
&out_len);
+  EXPECT_EQ(out_len, 7);
+  EXPECT_EQ(std::string(out_str, out_len), text_str + "  ");
+}
+
 TEST(TestStringOps, TestRtrim) {
   gandiva::ExecutionContext ctx;
   uint64_t ctx_ptr = reinterpret_cast<gdv_int64>(&ctx);

Reply via email to