From 5a297ef2954d9c6d35a41038aeffc20a6a661af5 Mon Sep 17 00:00:00 2001 From: Harsha Vardhan Date: Thu, 30 Jul 2026 13:07:07 +0530 Subject: [PATCH] GH-50707: [C++][Gandiva] Fix out-of-bounds read in mask functions --- cpp/src/gandiva/gdv_function_stubs.cc | 28 +++++++-- cpp/src/gandiva/gdv_function_stubs_test.cc | 69 ++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/cpp/src/gandiva/gdv_function_stubs.cc b/cpp/src/gandiva/gdv_function_stubs.cc index 94c5454cf24f..d01e81c72e97 100644 --- a/cpp/src/gandiva/gdv_function_stubs.cc +++ b/cpp/src/gandiva/gdv_function_stubs.cc @@ -484,7 +484,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(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)); @@ -592,7 +592,14 @@ 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(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(char_len); } @@ -606,7 +613,14 @@ 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(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'; @@ -695,7 +709,13 @@ 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(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 27ae60694b7d..0e404f84308e 100644 --- a/cpp/src/gandiva/gdv_function_stubs_test.cc +++ b/cpp/src/gandiva/gdv_function_stubs_test.cc @@ -1084,6 +1084,29 @@ TEST(TestGdvFnStubs, TestMaskFirstN) { expected = ""; result = gdv_mask_first_n_utf8_int32(ctx_ptr, data.c_str(), data_len, 6, &out_len); EXPECT_EQ(expected, std::string(result, out_len)); + + // Truncated 2-byte, 3-byte, and 4-byte UTF-8 sequences must report an error safely without OOB read + const char trunc2[] = {'a', static_cast(0xC2)}; + ctx.Reset(); + result = gdv_mask_first_n_utf8_int32(ctx_ptr, trunc2, 2, 2, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + + const char trunc3[] = {'a', static_cast(0xE2), static_cast(0x82)}; + ctx.Reset(); + result = gdv_mask_first_n_utf8_int32(ctx_ptr, trunc3, 3, 2, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + + const char trunc4[] = {'a', static_cast(0xF0), static_cast(0x9F), + static_cast(0x98)}; + ctx.Reset(); + result = gdv_mask_first_n_utf8_int32(ctx_ptr, trunc4, 4, 2, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); } TEST(TestGdvFnStubs, TestMaskLastN) { @@ -1149,6 +1172,29 @@ TEST(TestGdvFnStubs, TestMaskLastN) { expected = ""; result = gdv_mask_last_n_utf8_int32(ctx_ptr, data.c_str(), data_len, 6, &out_len); EXPECT_EQ(expected, std::string(result, out_len)); + + // Truncated 2-byte, 3-byte, and 4-byte UTF-8 sequences must report an error safely without OOB read + const char trunc2[] = {'a', static_cast(0xC2)}; + ctx.Reset(); + result = gdv_mask_last_n_utf8_int32(ctx_ptr, trunc2, 2, 2, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + + const char trunc3[] = {'a', static_cast(0xE2), static_cast(0x82)}; + ctx.Reset(); + result = gdv_mask_last_n_utf8_int32(ctx_ptr, trunc3, 3, 2, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + + const char trunc4[] = {'a', static_cast(0xF0), static_cast(0x9F), + static_cast(0x98)}; + ctx.Reset(); + result = gdv_mask_last_n_utf8_int32(ctx_ptr, trunc4, 4, 2, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); } TEST(TestGdvFnStubs, TestTranslate) { @@ -1502,6 +1548,29 @@ TEST(TestGdvFnStubs, TestMask) { result = mask_utf8_utf8_utf8_utf8(ctx_ptr, data.c_str(), data_len, "\?", 1, "*", 1, "#", 1, &out_len); EXPECT_EQ(std::string(result, out_len), expected); + + // Truncated 2-byte, 3-byte, and 4-byte UTF-8 sequences must report an error safely without OOB read + const char trunc2[] = {'a', static_cast(0xC2)}; + ctx.Reset(); + result = mask_utf8_utf8_utf8_utf8(ctx_ptr, trunc2, 2, "X", 1, "x", 1, "n", 1, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + + const char trunc3[] = {'a', static_cast(0xE2), static_cast(0x82)}; + ctx.Reset(); + result = mask_utf8_utf8_utf8_utf8(ctx_ptr, trunc3, 3, "X", 1, "x", 1, "n", 1, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); + + const char trunc4[] = {'a', static_cast(0xF0), static_cast(0x9F), + static_cast(0x98)}; + ctx.Reset(); + result = mask_utf8_utf8_utf8_utf8(ctx_ptr, trunc4, 4, "X", 1, "x", 1, "n", 1, &out_len); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(out_len, 0); + EXPECT_TRUE(ctx.has_error()); } TEST(TestGdvFnStubs, TestAesEncryptDecrypt16) {