[VL][MINOR] Tighten bounds check in SubstraitParser::checkWindowFunction#12086
[VL][MINOR] Tighten bounds check in SubstraitParser::checkWindowFunction#12086luis4a0 wants to merge 1 commit into
Conversation
The bounds check at SubstraitParser.cc:300 reads:
if ((pos != std::string::npos) &&
(msg.value().size() >= targetFunction.size()) &&
(msg.value().substr(pos + config.size(), targetFunction.size()) == targetFunction))
The size comparison (msg.value().size() >= targetFunction.size()) is
not enough to guarantee that the subsequent substr() call stays within
the buffer — the substr starts at offset pos + config.size(), so the
correct bound is
msg.value().size() >= pos + config.size() + targetFunction.size()
Today the bug is dormant: std::string::substr clamps to the actual
length and the resulting string compares unequal to the (longer)
target, so the function returns the right answer (false) for truncated
inputs by accident. There is no observable wrong behavior with any of
the current callers ("row_number", "rank", "dense_rank").
I'm sending this anyway because:
1. It's visibly wrong on inspection — reviewers later have to spend
cycles confirming whether it's a real bug.
2. std::string::substr's clamping behavior is the only thing keeping
this from being a runtime fault. A future refactor to
std::string_view::substr (different contract — throws
std::out_of_range on bad bounds) would turn this into a real
exception with no logical change to the surrounding code.
Adds cpp/velox/tests/SubstraitParserTest.cc with five cases: a
well-formed match, a well-formed non-match, the truncated-payload
regression case, an empty payload, and an extension with no
optimization. The test passes under both the buggy and the fixed
version (because the bug is dormant) — its purpose is to lock in
the desired behavior so any future refactor that breaks the bound
check fails loudly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Closing this — on second look (prompted by reviewer feedback to find a real failing case), the original bounds check is functionally correct, just stylistically odd.
Verified empirically with an exhaustive test across 90 (message, target) pairs including various truncations: zero behavioral differences between the two bound-check variants. So there's no bug to fix here, dormant or otherwise. Sorry for the noise; thanks for taking a look. |
What changes are proposed in this pull request?
The bounds check in
SubstraitParser::checkWindowFunction(
cpp/velox/substrait/SubstraitParser.cc:300) reads:if ((pos != std::string::npos) && (msg.value().size() >= targetFunction.size()) && (msg.value().substr(pos + config.size(), targetFunction.size()) == targetFunction))The size comparison
msg.value().size() >= targetFunction.size()isnot enough to guarantee that the subsequent
substr()call stayswithin the buffer — the substring starts at offset
pos + config.size(), so the correct bound isThis PR fixes the bound and adds a small
cpp/velox/tests/SubstraitParserTest.ccto lock in the desired behavior.
Is the bug observable today?
No. I want to be honest about this up front.
std::string::substrclamps the requested length to the actual remaining string length,
so the buggy bound + the substr call together produce the right
answer (false) for truncated inputs by accident. None of the current
callers (
"row_number","rank","dense_rank") exercise thefailure path in any way that produces a wrong result today.
I'm sending the fix anyway because:
cycles confirming whether it's a real bug. Cleaner to fix it now.
std::string::substr's clamping behaviour is the only thingkeeping this from being a runtime fault. A future refactor to
std::string_view::substr(different contract — throwsstd::out_of_rangeon bad bounds) would turn this into a realexception with no logical change to the surrounding code.
clear correctness argument.
How was this patch tested?
Adds
cpp/velox/tests/SubstraitParserTest.ccwith five cases:checkWindowFunctionWellFormedMatch— happy path.checkWindowFunctionWellFormedNoMatch— well-formed payload, target doesn't match.checkWindowFunctionTruncatedPayload— the regression case: payload bytesafter
window_function=are shorter than the target. Must return falsewithout overrunning the buffer.
checkWindowFunctionEmptyPayload— empty payload, nowindow_function=selector at all.checkWindowFunctionNoOptimization— extension with no optimization payload.All five tests pass under both the buggy and the fixed version, because
the bug is dormant (see "Is the bug observable today?" above). I validated
this with a standalone harness that compiles the same logic against both
versions of the bounds check (gated by
-DUSE_BUGGY=1):checkWindowFunctionWellFormedMatchcheckWindowFunctionWellFormedNoMatchcheckWindowFunctionTruncatedPayloadcheckWindowFunctionEmptyPayloadcheckWindowFunctionNoOptimizationSo the test is defensive only — its purpose is to lock in the
desired behavior so any future refactor that breaks the bound check
fails loudly, rather than to demonstrate the buggy version producing
wrong output today.
If reviewers prefer not to add a test for a dormant-bug case, I'm
happy to drop it and ship the 1-line fix on its own.
Marked as draft for one round of review before flipping to ready.