From e4905af9bb2c220470b3c4bfb56ce3f95d4f57e0 Mon Sep 17 00:00:00 2001 From: Rage Lopez Date: Fri, 21 Aug 2026 11:36:15 -0500 Subject: [PATCH 1/3] Fix integer overflow in mask_utf8_utf8_utf8_utf8 In mask_utf8_utf8_utf8_utf8, the output buffer size is computed as std::max(upper_length, std::max(lower_length, num_length)) * data_len without an overflow guard. The multiplication is done in int32_t, so a large replacement string combined with a large input wraps around, producing a small or negative max_length. The arena allocation is then far smaller than needed, causing a heap buffer overflow in the subsequent memcpy loop. Fix: use arrow::internal::MultiplyWithOverflow to detect the overflow and return an error, consistent with sibling functions (repeat_utf8_int32, to_hex_binary, etc.). Fixes #50472 --- cpp/src/gandiva/gdv_function_stubs.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/src/gandiva/gdv_function_stubs.cc b/cpp/src/gandiva/gdv_function_stubs.cc index 6b3e9935b017..c9d9e5d5ca58 100644 --- a/cpp/src/gandiva/gdv_function_stubs.cc +++ b/cpp/src/gandiva/gdv_function_stubs.cc @@ -28,6 +28,7 @@ #include "arrow/util/base64.h" #include "arrow/util/bit_util.h" #include "arrow/util/double_conversion_internal.h" +#include "arrow/util/int_util_overflow.h" #include "arrow/util/value_parsing.h" #include "gandiva/encrypt_utils.h" @@ -663,8 +664,14 @@ const char* mask_utf8_utf8_utf8_utf8(int64_t context, const char* data, int32_t return nullptr; } - int32_t max_length = - std::max(upper_length, std::max(lower_length, num_length)) * data_len; + int32_t max_length; + if (ARROW_PREDICT_FALSE(arrow::internal::MultiplyWithOverflow( + std::max(upper_length, std::max(lower_length, num_length)), data_len, + &max_length))) { + gdv_fn_context_set_error_msg(context, "Could not allocate memory for output string"); + *out_len = 0; + return nullptr; + } char* out = reinterpret_cast(gdv_fn_context_arena_malloc(context, max_length)); if (out == nullptr) { gdv_fn_context_set_error_msg(context, "Could not allocate memory for output string"); From 58d9cbbd7e455d6daff1029fc10296c7be49507e Mon Sep 17 00:00:00 2001 From: Rage Lopez Date: Fri, 21 Aug 2026 11:57:32 -0500 Subject: [PATCH 2/3] Fix out-of-bounds read in upper/lower/initcap on truncated UTF-8 gdv_fn_upper_utf8, gdv_fn_lower_utf8, and gdv_fn_initcap_utf8 call arrow::util::UTF8Decode without checking that the multibyte sequence fits within data_len. When the input ends in a truncated multibyte sequence, UTF8Decode reads past the buffer. Add bounds check before UTF8Decode, consistent with the invalid UTF-8 error handling. --- cpp/src/gandiva/gdv_string_function_stubs.cc | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cpp/src/gandiva/gdv_string_function_stubs.cc b/cpp/src/gandiva/gdv_string_function_stubs.cc index 55f5f13ac312..ecdd82b22051 100644 --- a/cpp/src/gandiva/gdv_string_function_stubs.cc +++ b/cpp/src/gandiva/gdv_string_function_stubs.cc @@ -289,6 +289,14 @@ const char* gdv_fn_lower_utf8(int64_t context, const char* data, int32_t data_le } // Control reaches here when we encounter a multibyte character + // Ensure the multibyte sequence fits within the buffer to avoid + // reading past data_len (truncated trailing multibyte sequence). + if (i + char_len > data_len) { + gdv_fn_set_error_for_invalid_utf8(context, data[i]); + *out_len = 0; + return ""; + } + const auto* in_char = (const uint8_t*)(data + i); // Decode the multibyte character @@ -366,6 +374,14 @@ const char* gdv_fn_upper_utf8(int64_t context, const char* data, int32_t data_le } // Control reaches here when we encounter a multibyte character + // Ensure the multibyte sequence fits within the buffer to avoid + // reading past data_len (truncated trailing multibyte sequence). + if (i + char_len > data_len) { + gdv_fn_set_error_for_invalid_utf8(context, data[i]); + *out_len = 0; + return ""; + } + const auto* in_char = (const uint8_t*)(data + i); // Decode the multibyte character @@ -584,6 +600,14 @@ const char* gdv_fn_initcap_utf8(int64_t context, const char* data, int32_t data_ char_len = gdv_fn_utf8_char_length(data[i]); // Control reaches here when we encounter a multibyte character + // Ensure the multibyte sequence fits within the buffer to avoid + // reading past data_len (truncated trailing multibyte sequence). + if (i + char_len > data_len) { + gdv_fn_set_error_for_invalid_utf8(context, data[i]); + *out_len = 0; + return ""; + } + const auto* in_char = (const uint8_t*)(data + i); // Decode the multibyte character From dd47db3afcd06f1b2737bc31250d99a2ee113ec9 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 21 Sep 2026 00:24:56 -0500 Subject: [PATCH 3/3] Test Gandiva mask overflow and truncated UTF-8 conversion Avoid addition overflow in the slice bound checks. Cover output-size multiplication and partial multibyte characters, and correct a valid InitCap fixture that supplied 19 for a 20-byte input. Assisted-by: Codex:GPT-6 --- cpp/src/gandiva/gdv_function_stubs_test.cc | 40 +++++++++++++++++++- cpp/src/gandiva/gdv_string_function_stubs.cc | 6 +-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/cpp/src/gandiva/gdv_function_stubs_test.cc b/cpp/src/gandiva/gdv_function_stubs_test.cc index c0197eb815c8..a85370fdc7fb 100644 --- a/cpp/src/gandiva/gdv_function_stubs_test.cc +++ b/cpp/src/gandiva/gdv_function_stubs_test.cc @@ -805,7 +805,7 @@ TEST(TestGdvFnStubs, TestInitCap) { EXPECT_EQ(std::string(out_str, out_len), "{Õhp,Pqśv}Ń+"); EXPECT_FALSE(ctx.has_error()); - out_str = gdv_fn_initcap_utf8(ctx_ptr, "sɦasasdsɦsd\"sdsdɦ", 19, &out_len); + out_str = gdv_fn_initcap_utf8(ctx_ptr, "sɦasasdsɦsd\"sdsdɦ", 20, &out_len); EXPECT_EQ(std::string(out_str, out_len), "Sɦasasdsɦsd\"Sdsdɦ"); EXPECT_FALSE(ctx.has_error()); @@ -1188,6 +1188,44 @@ TEST(TestGdvFnStubs, TestMaskTruncatedUtf8NoOverread) { EXPECT_TRUE(ctx.has_error()); } +TEST(TestGdvFnStubs, TestMaskOutputLengthOverflow) { + gandiva::ExecutionContext ctx; + auto ctx_ptr = reinterpret_cast(&ctx); + int32_t out_len = -1; + const std::string data(65536, 'A'); + const std::string replacement(65536, 'X'); + auto result = mask_utf8_utf8_utf8_utf8( + ctx_ptr, data.data(), static_cast(data.size()), replacement.data(), + static_cast(replacement.size()), "x", 1, "n", 1, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); +} + +TEST(TestGdvFnStubs, TestCaseConversionTruncatedUtf8) { + const std::string inputs[] = {"a\xc2\xa2", "a\xe2\x82\xac", "a\xf0\x9f\x98\x80"}; + for (auto convert : {gdv_fn_lower_utf8, gdv_fn_upper_utf8, gdv_fn_initcap_utf8}) { + for (const auto& input : inputs) { + const auto full_len = static_cast(input.size()); + for (int32_t len = 2; len < full_len; ++len) { + gandiva::ExecutionContext ctx; + auto ctx_ptr = reinterpret_cast(&ctx); + int32_t out_len = -1; + // Bytes beyond len complete the glyph, but are outside the input slice. + convert(ctx_ptr, input.data(), len, &out_len); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + } + gandiva::ExecutionContext ctx; + auto ctx_ptr = reinterpret_cast(&ctx); + int32_t out_len = -1; + convert(ctx_ptr, input.data(), full_len, &out_len); + EXPECT_GT(out_len, 0); + EXPECT_FALSE(ctx.has_error()); + } + } +} + TEST(TestGdvFnStubs, TestTranslate) { gandiva::ExecutionContext ctx; int64_t ctx_ptr = reinterpret_cast(&ctx); diff --git a/cpp/src/gandiva/gdv_string_function_stubs.cc b/cpp/src/gandiva/gdv_string_function_stubs.cc index ecdd82b22051..fc18bc0fac01 100644 --- a/cpp/src/gandiva/gdv_string_function_stubs.cc +++ b/cpp/src/gandiva/gdv_string_function_stubs.cc @@ -291,7 +291,7 @@ const char* gdv_fn_lower_utf8(int64_t context, const char* data, int32_t data_le // Control reaches here when we encounter a multibyte character // Ensure the multibyte sequence fits within the buffer to avoid // reading past data_len (truncated trailing multibyte sequence). - if (i + char_len > data_len) { + if (char_len > data_len - i) { gdv_fn_set_error_for_invalid_utf8(context, data[i]); *out_len = 0; return ""; @@ -376,7 +376,7 @@ const char* gdv_fn_upper_utf8(int64_t context, const char* data, int32_t data_le // Control reaches here when we encounter a multibyte character // Ensure the multibyte sequence fits within the buffer to avoid // reading past data_len (truncated trailing multibyte sequence). - if (i + char_len > data_len) { + if (char_len > data_len - i) { gdv_fn_set_error_for_invalid_utf8(context, data[i]); *out_len = 0; return ""; @@ -602,7 +602,7 @@ const char* gdv_fn_initcap_utf8(int64_t context, const char* data, int32_t data_ // Control reaches here when we encounter a multibyte character // Ensure the multibyte sequence fits within the buffer to avoid // reading past data_len (truncated trailing multibyte sequence). - if (i + char_len > data_len) { + if (char_len > data_len - i) { gdv_fn_set_error_for_invalid_utf8(context, data[i]); *out_len = 0; return "";