Fix Debug assert in IntervalTree's is_valid() with til::point keys - #20487
Fix Debug assert in IntervalTree's is_valid() with til::point keys#20487steffen-heil-secforge wants to merge 2 commits into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
This comment has been minimized.
This comment has been minimized.
is_valid() seeded its bounds accumulators with std::numeric_limits<Scalar>
::max()/min(). We instantiate the tree with Scalar = til::point, which has
no std::numeric_limits specialization, so the primary template returned a
default-constructed til::point{0,0} for *both* sentinels. The right-subtree
constraint check then evaluated {0,0} <= center, which holds for essentially
any real tree, so is_valid() returned false and assert(is_valid().first)
fired.
The tree only splits into subtrees once it holds 64 intervals, its default
minimum bucket size, so the threshold was exactly 64: Terminal::_getPatterns
builds the pattern tree from autodetected URLs over roughly three viewport-
heights of buffer, making 64 matches easy to reach. Any Debug build showing
a screenful of links aborted. Release builds were unaffected, and the tree
itself was structurally sound -- only the checker was wrong.
Track emptiness explicitly so no sentinel value is needed. Also accumulate
the maximum stop with std::max rather than std::min; upstream's std::min
pinned that accumulator at numeric_limits<Scalar>::min() forever, silently
reducing both center checks to no-ops, so a numeric_limits<til::point>
specialization alone would have left the check dead.
This is the second local deviation from ekg/intervaltree, alongside the
struct-key support carried since microsoft#7691 (upstream PR ekg/intervaltree#31,
still unmerged). Both are now recorded in MAINTAINER_README.md, together
with a warning that re-importing the file wholesale breaks the build.
Closes microsoft#20486
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pr3nsYBYoJ66jgRidRHyJF
eb587fd to
146c788
Compare
|
Can you edit the PR description to have a bit more of a human touch? It's longer than the diff lol. |
|
Ha, fair. Trimmed it by half and cut the bullet-point wall — the vendored-file justification and the "I couldn't build this on Windows" caveat are the two bits I kept, since I figured you'd want both up front. |
lhecker
left a comment
There was a problem hiding this comment.
As discussed, I'd prefer a specialization of til::point, til::size and so on. Regardless, this is an excellent find. Thank you for it!
…rval_tree Per review feedback, revert the change to oss/interval_tree/IntervalTree.h and fix the root assumption on our side instead: is_valid() requires std::numeric_limits<Scalar> to be meaningful, so specialize it for the til coordinate types rather than teaching the vendored file to do without. The vendored header is now byte-identical to what it was before this branch. Also add <limits> to LibraryIncludes.h. It was not included anywhere, and relying on a transitive include to declare the primary template before specializing it is fragile. Note this fixes the assert without making is_valid() correct: upstream accumulates the maximum stop with std::min, so that accumulator stays pinned at numeric_limits<Scalar>::min() and the left-subtree check can never fire. The right-subtree check reads the min-start accumulator and works. That trade-off is accepted deliberately -- see the PR discussion. Verified Debug|x64: IntervalTreeTests 3/3, full til suite 281/281. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pr3nsYBYoJ66jgRidRHyJF
|
Done — specialized And sorry for having Claude write the texts. I do review them (code and messages), but his English is better than mine, and more precise. |
assert(is_valid().first)inoss/interval_tree/IntervalTree.hfires in Debug builds whenever 64 or more URLs get autodetected on screen.for /l %i in (1,1,80) do @echo https://example.com/%iis enough to do it.is_valid()seeds its bounds accumulators withstd::numeric_limits<Scalar>::max()/::min(). We instantiate the tree withScalar = til::point, which has no specialization, so the primary template hands back a default-constructedtil::point{0,0}for both sentinels. The right-subtree check then asks{0,0} <= center, which is true for any real tree, so validation fails. 64 is the magic number because that's where the tree starts splitting into subtrees at all.The fix is to stop needing sentinels — track emptiness explicitly instead. I also switched the max-stop accumulator from
std::mintostd::max, which is a separate upstream bug and the reason nobody upstream ever hit this:std::minpins that accumulator atnumeric_limits<Scalar>::min()forever, so the left-subtree check (valid.second.second >= center) can never fire. You can see it by printing whatis_valid()returns — for a tree whose real maximum stop is{10,99}, upstream's accumulator comes back as{INT32_MIN,INT32_MIN}. The right-subtree check reads the min-start accumulator and is unaffected.That's also the trade-off against just specializing
std::numeric_limits<til::point>and leaving this file alone (see @lhecker's comment below): that does fix the crash, but the max-stop accumulator stays dead, so the left-subtree constraint is never actually checked. Happy to go that route instead if you prefer the smaller footprint — it's your call.Release builds were unaffected and the tree itself was always structurally valid — only the checker was wrong.
On editing a vendored file:
MAINTAINER_README.mdsays take it wholesale and change nothing, so this needs an excuse. The file already isn't upstream's, though — every existing difference comes from ekg/intervaltree#31, opened by @PankajBhojwani in 2020 and vendored here 27 days later, still unmerged. Without it this doesn't compile againsttil::pointat all, and it's also what made this bug reachable: it taught the constructor about struct keys but notis_valid(). Upstream's last commit was March 2021, so there's nothing to wait for. Both deviations are now written down in the README, along with a warning that re-importing wholesale will break the build.Two things I left alone but can fold in:
cgmanifest.jsonpins a hash whose content doesn't match the file, and I haven't reported thestd::minbug upstream.Built and tested locally,
Debug|x64: the newIntervalTreeTestspass (2/2), and the full til suite is 280/280 with no regressions. Reverting just the header makes the new test hit the assert, though it does so by blocking on the assert dialog rather than failing cleanly.Closes #20486