Skip to content

Commit e2b7c99

Browse files
lriggsclaude
andcommitted
Fix REPLACE buffer overflow for large output strings
Gandiva's REPLACE hardcoded a 65535-byte output cap, throwing "Buffer overflow for output string" whenever the result exceeded 64 KB. Size the output buffer to the exact result instead, by counting non-overlapping matches of from_str: text_len + num_matches * (to_str_len - from_str_len). Removes the arbitrary cap; the internal replace_with_max_len variant and its bounds checks are unchanged. Gandiva variable-length output uses int32 offsets, so a single output string cannot exceed INT_MAX (2 GB). Guard that boundary explicitly with a clear error message instead of letting the int32 size cast wrap silently (which could otherwise lead to under-allocation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a81e6c6 commit e2b7c99

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

cpp/src/gandiva/precompiled/string_ops.cc

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,9 +1914,33 @@ const char* replace_utf8_utf8_utf8(gdv_int64 context, const char* text,
19141914
gdv_int32 text_len, const char* from_str,
19151915
gdv_int32 from_str_len, const char* to_str,
19161916
gdv_int32 to_str_len, gdv_int32* out_len) {
1917+
// Count non-overlapping matches to size the output buffer exactly, so large
1918+
// results are not capped by an arbitrary limit.
1919+
gdv_int64 num_matches = 0;
1920+
if (from_str_len > 0 && from_str_len <= text_len) {
1921+
for (gdv_int32 i = 0; i <= text_len - from_str_len;) {
1922+
if (memcmp(text + i, from_str, from_str_len) == 0) {
1923+
num_matches++;
1924+
i += from_str_len;
1925+
} else {
1926+
i++;
1927+
}
1928+
}
1929+
}
1930+
gdv_int64 max_length =
1931+
static_cast<gdv_int64>(text_len) + num_matches * (to_str_len - from_str_len);
1932+
// Gandiva variable-length output uses int32 offsets, so a single output string
1933+
// cannot exceed INT_MAX bytes. Report this explicitly instead of letting the
1934+
// cast below wrap silently.
1935+
if (max_length > INT_MAX) {
1936+
gdv_fn_context_set_error_msg(context,
1937+
"REPLACE: output string exceeds maximum size of 2GB");
1938+
*out_len = 0;
1939+
return "";
1940+
}
19171941
return replace_with_max_len_utf8_utf8_utf8(context, text, text_len, from_str,
1918-
from_str_len, to_str, to_str_len, 65535,
1919-
out_len);
1942+
from_str_len, to_str, to_str_len,
1943+
static_cast<gdv_int32>(max_length), out_len);
19201944
}
19211945

19221946
// Returns the quoted string (Includes escape character for any single quotes)

cpp/src/gandiva/precompiled/string_ops_test.cc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,42 @@ TEST(TestStringOps, TestReplace) {
19711971
EXPECT_EQ(std::string(out_str, out_len), "TestString");
19721972
EXPECT_FALSE(ctx.has_error());
19731973

1974+
// Large output (>64 KB) must not overflow: buffer is sized to the exact result.
1975+
std::string large_in(35000, 'X');
1976+
std::string large_expected(70000, '\0');
1977+
for (int i = 0; i < 35000; ++i) {
1978+
large_expected[2 * i] = 'X';
1979+
large_expected[2 * i + 1] = 'Y';
1980+
}
1981+
out_str = replace_utf8_utf8_utf8(ctx_ptr, large_in.data(),
1982+
static_cast<int32_t>(large_in.size()), "X", 1, "XY", 2,
1983+
&out_len);
1984+
EXPECT_EQ(out_len, 70000);
1985+
EXPECT_EQ(std::string(out_str, out_len), large_expected);
1986+
EXPECT_FALSE(ctx.has_error());
1987+
1988+
// Large shrinking output ("XX" -> "X") on a >64 KB input.
1989+
std::string large_shrink_in(70000, 'X');
1990+
std::string large_shrink_expected(35000, 'X');
1991+
out_str = replace_utf8_utf8_utf8(ctx_ptr, large_shrink_in.data(),
1992+
static_cast<int32_t>(large_shrink_in.size()), "XX", 2,
1993+
"X", 1, &out_len);
1994+
EXPECT_EQ(out_len, 35000);
1995+
EXPECT_EQ(std::string(out_str, out_len), large_shrink_expected);
1996+
EXPECT_FALSE(ctx.has_error());
1997+
1998+
// Output that would exceed INT_MAX (2GB) is reported cleanly rather than
1999+
// silently wrapping the int32 size. 50000 matches each expanding to 50000
2000+
// bytes implies max_length = 2.5e9; the guard fires before any large alloc.
2001+
std::string huge_in(50000, 'X');
2002+
std::string huge_to(50000, 'Z');
2003+
replace_utf8_utf8_utf8(ctx_ptr, huge_in.data(), static_cast<int32_t>(huge_in.size()),
2004+
"X", 1, huge_to.data(), static_cast<int32_t>(huge_to.size()),
2005+
&out_len);
2006+
EXPECT_THAT(ctx.get_error(), ::testing::HasSubstr("exceeds maximum size"));
2007+
EXPECT_EQ(out_len, 0);
2008+
ctx.Reset();
2009+
19742010
replace_with_max_len_utf8_utf8_utf8(ctx_ptr, "Hell", 4, "ell", 3, "ollow", 5, 5,
19752011
&out_len);
19762012
EXPECT_THAT(ctx.get_error(), ::testing::HasSubstr("Buffer overflow for output string"));

0 commit comments

Comments
 (0)