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 45801eb4175 GH-50440: [C++][Gandiva] Fix out-of-bounds read in
set_error_for_date (#50441)
45801eb4175 is described below
commit 45801eb41757eed06c3ed34eadd92f35e1ce3741
Author: Abdul Rawoof Khan <[email protected]>
AuthorDate: Thu Oct 1 12:18:30 2026 +0530
GH-50440: [C++][Gandiva] Fix out-of-bounds read in set_error_for_date
(#50441)
### Rationale for this change
`set_error_for_date` formats the invalid-date/timestamp message with
`snprintf(error, size, "%s%s", msg, input)`, but `input` is a Gandiva string
passed as pointer plus `length` and is not NUL-terminated, so the `%s`
conversion scans past the `length` bytes looking for a terminator and
over-reads the values buffer (past its end for the last value). It is reached
from `castDATE_utf8`/`castTIMESTAMP_utf8` on any invalid `CAST(str AS
DATE/TIMESTAMP)` input. Switching to the bounded `%.*s [...]
### What changes are included in this PR?
Use `%.*s` with `length` in `set_error_for_date`, which covers all of its
call sites in both `castDATE_utf8` and `castTIMESTAMP_utf8`.
### Are these changes tested?
Yes. Added `TestTime.TestCastDateInvalidUnterminated`, which feeds an
invalid date held in an exactly-sized heap buffer with no trailing NUL so the
over-read trips AddressSanitizer, and checks the error string is bounded to the
input length.
### Are there any user-facing changes?
No.
**This PR contains a "Critical Fix".** It fixes a heap out-of-bounds read
reachable from user-supplied CAST-to-date/timestamp input.
* GitHub Issue: #50440
Authored-by: abdul rawoof <[email protected]>
Signed-off-by: Sutou Kouhei <[email protected]>
---
cpp/src/gandiva/precompiled/time.cc | 2 +-
cpp/src/gandiva/precompiled/time_test.cc | 54 ++++++++++++++++++++++++++++++++
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/cpp/src/gandiva/precompiled/time.cc
b/cpp/src/gandiva/precompiled/time.cc
index 2b60f63651d..9a66e41bbb1 100644
--- a/cpp/src/gandiva/precompiled/time.cc
+++ b/cpp/src/gandiva/precompiled/time.cc
@@ -631,7 +631,7 @@ void set_error_for_date(gdv_int32 length, const char*
input, const char* msg,
int64_t execution_context) {
int size = length + static_cast<int>(strlen(msg)) + 1;
char* error = reinterpret_cast<char*>(malloc(size));
- snprintf(error, size, "%s%s", msg, input);
+ snprintf(error, size, "%s%.*s", msg, length, input);
gdv_fn_context_set_error_msg(execution_context, error);
free(error);
}
diff --git a/cpp/src/gandiva/precompiled/time_test.cc
b/cpp/src/gandiva/precompiled/time_test.cc
index 4310490a228..a45aa353218 100644
--- a/cpp/src/gandiva/precompiled/time_test.cc
+++ b/cpp/src/gandiva/precompiled/time_test.cc
@@ -18,6 +18,10 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
+#include <cstring>
+#include <memory>
+#include <string>
+
#include "arrow/util/logging_internal.h"
#include "gandiva/execution_context.h"
#include "gandiva/precompiled/testing.h"
@@ -59,6 +63,56 @@ TEST(TestTime, TestCastDate) {
EXPECT_EQ(castDATE_date32(1), 86400000);
}
+TEST(TestTime, TestCastDateInvalidUnterminated) {
+ ExecutionContext context;
+ int64_t context_ptr = reinterpret_cast<int64_t>(&context);
+
+ // The invalid-value error message is built from the raw input pointer. Hold
+ // the input in an exactly-sized heap buffer with no trailing NUL so that
+ // formatting the error must respect `length` instead of scanning for a NUL;
+ // any over-read past the buffer trips AddressSanitizer.
+ const std::string value = "197201234";
+ const auto length = static_cast<int32_t>(value.size());
+ std::unique_ptr<char[]> input(new char[length]);
+ std::memcpy(input.get(), value.data(), length);
+
+ EXPECT_EQ(castDATE_utf8(context_ptr, input.get(), length), 0);
+ EXPECT_EQ(context.get_error(), "Not a valid date value 197201234");
+ context.Reset();
+}
+
+TEST(TestTime, TestCastTimestampInvalidUnterminated) {
+ ExecutionContext context;
+ int64_t context_ptr = reinterpret_cast<int64_t>(&context);
+
+ // castTIMESTAMP_utf8 reaches the same set_error_for_date helper, so exercise
+ // the over-read path from that entry point with an unterminated buffer too.
+ const std::string value = "2000-01-01 24:00:00";
+ const auto length = static_cast<int32_t>(value.size());
+ std::unique_ptr<char[]> input(new char[length]);
+ std::memcpy(input.get(), value.data(), length);
+
+ EXPECT_EQ(castTIMESTAMP_utf8(context_ptr, input.get(), length), 0);
+ EXPECT_EQ(context.get_error(),
+ "Not a valid time for timestamp value 2000-01-01 24:00:00");
+ context.Reset();
+}
+
+TEST(TestTime, TestCastTimeInvalidUnterminated) {
+ ExecutionContext context;
+ int64_t context_ptr = reinterpret_cast<int64_t>(&context);
+
+ // castTIME_utf8 reaches set_error_for_date as well.
+ const std::string value = "24H00H00";
+ const auto length = static_cast<int32_t>(value.size());
+ std::unique_ptr<char[]> input(new char[length]);
+ std::memcpy(input.get(), value.data(), length);
+
+ EXPECT_EQ(castTIME_utf8(context_ptr, input.get(), length), 0);
+ EXPECT_EQ(context.get_error(), "Invalid character in time 24H00H00");
+ context.Reset();
+}
+
TEST(TestTime, TestCastTimestamp) {
ExecutionContext context;
int64_t context_ptr = reinterpret_cast<int64_t>(&context);