Description
cudf::strings::is_timestamp reads one byte past the end of a string when the format ends with a literal and the input ends at the last field. The verdict for such a row then depends on whatever byte follows in the chars buffer, so the same input can be reported valid in one run and invalid in another.
In cpp/src/strings/convert/convert_datetime.cu, check_datetime_format::check_string dereferences ptr in the literal branch with no bounds check:
auto ptr = d_string.data();
auto length = d_string.size_bytes();
for (auto item : d_format_items) {
// eliminate static character values first
if (item.item_type == format_char_type::literal) {
// check static character matches
if (*ptr != item.value) return cuda::std::nullopt; // <-- no `length > 0` guard
ptr += item.length;
length -= item.length;
continue;
}
length is decremented but never tested here, so it can also go negative and the loop continues past the end of the string.
The specifier branches below are guarded — they clamp with item.length = min(item.length, length) under the comment "allow for specifiers to be truncated" — so only a literal is affected.
Reproducer
Format "%Y-%m-%d %H:%M:%S " (note the trailing space) against the 19-byte input "2021-01-01 02:00:00". After %S consumes the last two digits, ptr is one past the end and length is 0; the trailing space literal is then compared against out-of-bounds memory.
Impact
Observed while building GPU timezone functions in Velox (facebookincubator/velox#17899). A parse_datetime format carrying a trailing zone token produces exactly this shape: the zone token is parsed separately, so the format handed to is_timestamp keeps the literal that preceded it. The same input reported two different validation errors across two builds of unchanged code, which is what led to this.
Nothing is written, so this is an out-of-bounds read rather than corruption, but the result is non-deterministic and it is reachable from ordinary input.
Suggested fix
Guard the literal comparison the way the specifier branches are guarded — return nullopt when no input remains:
if (item.item_type == format_char_type::literal) {
if (length < item.length) return cuda::std::nullopt;
if (*ptr != item.value) return cuda::std::nullopt;
...
Version
cuDF commit 5beaa59 on release/26.08, as pinned by Velox (CMake/resolve_dependency_modules/cudf.cmake). The code is unchanged on branch-26.08 as far as I can tell from the file above, but I have not tested a newer build.
Description
cudf::strings::is_timestampreads one byte past the end of a string when the format ends with a literal and the input ends at the last field. The verdict for such a row then depends on whatever byte follows in the chars buffer, so the same input can be reported valid in one run and invalid in another.In
cpp/src/strings/convert/convert_datetime.cu,check_datetime_format::check_stringdereferencesptrin the literal branch with no bounds check:lengthis decremented but never tested here, so it can also go negative and the loop continues past the end of the string.The specifier branches below are guarded — they clamp with
item.length = min(item.length, length)under the comment "allow for specifiers to be truncated" — so only a literal is affected.Reproducer
Format
"%Y-%m-%d %H:%M:%S "(note the trailing space) against the 19-byte input"2021-01-01 02:00:00". After%Sconsumes the last two digits,ptris one past the end andlengthis 0; the trailing space literal is then compared against out-of-bounds memory.Impact
Observed while building GPU timezone functions in Velox (facebookincubator/velox#17899). A
parse_datetimeformat carrying a trailing zone token produces exactly this shape: the zone token is parsed separately, so the format handed tois_timestampkeeps the literal that preceded it. The same input reported two different validation errors across two builds of unchanged code, which is what led to this.Nothing is written, so this is an out-of-bounds read rather than corruption, but the result is non-deterministic and it is reachable from ordinary input.
Suggested fix
Guard the literal comparison the way the specifier branches are guarded — return
nulloptwhen no input remains:Version
cuDF commit
5beaa59onrelease/26.08, as pinned by Velox (CMake/resolve_dependency_modules/cudf.cmake). The code is unchanged onbranch-26.08as far as I can tell from the file above, but I have not tested a newer build.