fix(jit_allocator): bound BitVectorRangeIterator range_start to _end#3
Merged
Merged
Conversation
BitVectorRangeIterator::next_range() did not clamp *range_start against
_end. The iterator's init() only masks bits before start, so the
underlying BitWord can carry free bits past end. When ctz picked such a
bit, the caller would compute range_size = range_end - range_start as
size_t (range_end gets clamped to _end), the subtraction would underflow
to a huge value, and a range that lies entirely past the search region
would be accepted.
Inside JitAllocator::alloc this produced an area_index outside the
block: the returned Span reported its requested size even though the
underlying memory ran past the block boundary into unmapped pages.
QuestDB caught this as a SIGSEGV inside JitRuntime::_add's memcpy: a
production core dump shows a block with _area_size=4096,
_search_end=4083 and 3 free bits at the tail (areas 4093-4095). A
7-area request let the iterator pick range_start=4093 with range_end
clamped to 4083, _area_used over-incremented by 4, the Span claimed 448
bytes when only 192 were valid, and the JIT runtime memcpy walked off
the end of the block.
The fix adds a single bound check: if *range_start >= _end after the
ctz, the iterator reports end-of-iteration. Two regression tests cover
the path:
- test_bit_vector_range_iterator_bounds() exercises the iterator
directly with two hand-crafted bitmaps (same-word and multi-word
search regions). Without the fix EXPECT_LT(range_start, end)
fails on the first iterator call.
- test_jit_allocator_search_end_bounds() drives JitAllocator into
the production state (area_used = area_size - 14, free bits both
inside the search range and at the tail past _search_end). With
the bug the alloc trips the existing offset <= block_size - size
assertion at line ~999; with the fix the search reports no fit and
a fresh block is allocated.
bluestreak01
approved these changes
May 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BitVectorRangeIterator::next_range()did not clamp*range_startagainst_end. The iterator'sinit()only masks bits before start, so the underlyingBitWordcan carry free bits past end. Whenctzpicked such a bit, the caller would computerange_size = range_end - range_startassize_t(range_endgets clamped to_end), the subtraction would underflow to a huge value, and a range that lies entirely past the search region would be accepted.Inside
JitAllocator::allocthis produced anarea_indexoutside the block: the returnedSpanreported its requested size even though the underlying memory ran past the block boundary into unmapped pages. QuestDB caught this as a SIGSEGV insideJitRuntime::_add's memcpy: a production core dump shows a block with _area_size=4096, _search_end=4083 and 3 free bits at the tail (areas 4093-4095). A 7-area request let the iterator pick range_start=4093 with range_end clamped to 4083, _area_used over-incremented by 4, the Span claimed 448 bytes when only 192 were valid, and the JIT runtime memcpy walked off the end of the block.The fix adds a single bound check:
if *range_start >= _endafter thectz, the iterator reports end-of-iteration. Two regression tests cover the path:test_bit_vector_range_iterator_bounds()exercises the iterator directly with two hand-crafted bitmaps (same-word and multi-word search regions). Without the fixEXPECT_LT(range_start, end)fails on the first iterator call.test_jit_allocator_search_end_bounds()drivesJitAllocatorinto the production state (area_used = area_size - 14, free bits both inside the search range and at the tail past _search_end). With the bug the alloc trips the existing offset <= block_size - size assertion at line ~999; with the fix the search reports no fit and a fresh block is allocated.