@@ -107,7 +107,13 @@ class BitVectorRangeIterator {
107107
108108 size_t i = Support::ctz (_bit_word);
109109
110- *range_start = _idx + i;
110+ size_t start = _idx + i;
111+ if (ASMJIT_UNLIKELY (start >= _end)) {
112+ // `init()` only masks bits before `start`, so the current BitWord can
113+ // carry free bits past `_end` and `ctz` would return such a position.
114+ return false ;
115+ }
116+ *range_start = start;
111117 _bit_word = ~(_bit_word ^ ~(Support::bit_ones<T> << i));
112118
113119 if (_bit_word == 0 ) {
@@ -1450,6 +1456,129 @@ static void BitVectorRangeIterator_testRandom(TestUtils::Random& rnd, size_t cou
14501456 }
14511457}
14521458
1459+ // Regression test for BitVectorRangeIterator returning ranges past `end`.
1460+ // `init()` only masks bits before `start`, so a BitWord can carry free bits
1461+ // past `end` and ctz could pick one - see next_range() for the bound check.
1462+ static void test_bit_vector_range_iterator_bounds () noexcept {
1463+ using Bw = Support::BitWord;
1464+ constexpr Bw all_ones = Support::bit_ones<Bw>;
1465+ constexpr size_t kBwBits = Support::bit_size_of<Bw>;
1466+
1467+ // Case 1: start and end share the same BitWord; free bits live past end-in-word.
1468+ // bitmap: bits 0..kBwBits/2-1 used, rest free; search [8, 16). Without the
1469+ // fix, ctz picks bit kBwBits/2 (16 or 32) -> range_start >= end.
1470+ {
1471+ Bw bitmap[1 ];
1472+ bitmap[0 ] = (Bw (1 ) << (kBwBits / 2u )) - Bw (1 );
1473+ BitVectorRangeIterator<Bw, 0 > it (bitmap, 1u , 8u , 16u );
1474+ size_t s = 0 , e = 0 ;
1475+ while (it.next_range (Out (s), Out (e))) {
1476+ EXPECT_LT (s, 16u );
1477+ EXPECT_LE (e, 16u );
1478+ EXPECT_LE (s, e);
1479+ }
1480+ }
1481+
1482+ // Case 2: end mid-word in the last BitWord; mirrors a production state.
1483+ // Last 3 bits of the bitmap are free, end is placed 13 bits before total
1484+ // (mid last word). Without the fix, iterator returns range_start at the
1485+ // tail free bits >= end.
1486+ {
1487+ constexpr size_t kWordCount = 64 ;
1488+ constexpr size_t kTotalBits = kWordCount * kBwBits ;
1489+ constexpr size_t kEnd = kTotalBits - 13u ;
1490+ Bw bitmap[kWordCount ];
1491+ for (size_t i = 0 ; i < kWordCount ; i++) {
1492+ bitmap[i] = all_ones;
1493+ }
1494+ bitmap[kWordCount - 1u ] = ~(Bw (0x7 ) << (kBwBits - 3u ));
1495+
1496+ BitVectorRangeIterator<Bw, 0 > it (bitmap, kWordCount , 72u , kEnd );
1497+ size_t s = 0 , e = 0 ;
1498+ while (it.next_range (Out (s), Out (e))) {
1499+ EXPECT_LT (s, kEnd );
1500+ EXPECT_LE (e, kEnd );
1501+ EXPECT_LE (s, e);
1502+ }
1503+ }
1504+ }
1505+
1506+ // Regression test for JitAllocator returning an oversized Span when bitmap
1507+ // search finds a range past the block boundary. Mirrors a QuestDB production
1508+ // core dump: an almost-full block where the iterator can match the last few
1509+ // free areas at the tail even though the search-region end is set below them.
1510+ //
1511+ // The state isn't reachable through the public API in a few calls, so this
1512+ // test pokes JitAllocatorBlock internals directly and must move with any
1513+ // block-bookkeeping refactor.
1514+ static void test_jit_allocator_search_end_bounds () noexcept {
1515+ JitAllocator allocator;
1516+
1517+ // Force a block to exist so we can hand-craft state on it.
1518+ JitAllocator::Span anchor_span;
1519+ EXPECT_EQ (allocator.alloc (Out (anchor_span), 64u ), Error::kOk );
1520+ EXPECT_NOT_NULL (anchor_span.rx ());
1521+
1522+ JitAllocatorBlock* block = static_cast <JitAllocatorBlock*>(anchor_span._block );
1523+ EXPECT_NOT_NULL (block);
1524+
1525+ JitAllocatorPool* pool = block->pool ();
1526+ uint32_t area_size = block->area_size ();
1527+ uint32_t granularity = pool->granularity ;
1528+
1529+ // Need enough room to place a search range with fragmented free bits inside
1530+ // and 3 free bits at the tail past _search_end.
1531+ if (area_size < 256u ) {
1532+ return ;
1533+ }
1534+
1535+ // Production state: 14 free areas total. 11 are fragmented inside the
1536+ // search range with no contiguous run >= 7 (forces the search to the last
1537+ // BitWord). The other 3 sit at the tail past _search_end - the buggy
1538+ // iterator picks them up and the caller takes the underflowed range_size
1539+ // as an oversize fit.
1540+ Support::bit_vector_fill (block->_used_bit_vector , 0u , area_size);
1541+
1542+ // Fragmented free areas inside the search range, none contiguous >= 7.
1543+ uint32_t mid = area_size / 2u ;
1544+ Support::bit_vector_clear (block->_used_bit_vector , mid - 64u , 5u );
1545+ Support::bit_vector_clear (block->_used_bit_vector , mid, 1u );
1546+ Support::bit_vector_clear (block->_used_bit_vector , mid + 64u , 5u );
1547+ // 3 free areas past _search_end.
1548+ Support::bit_vector_clear (block->_used_bit_vector , area_size - 3u , 3u );
1549+
1550+ // Anchor sentinel - placement doesn't matter for triggering the bug. The
1551+ // inconsistent _used/_stop state is safe at teardown: ~JitAllocator ->
1552+ // reset(kHard) walks pool.blocks and frees each without traversing sentinels.
1553+ Support::bit_vector_set_bit (block->_stop_bit_vector , area_size - 4u , true );
1554+
1555+ block->_area_used = area_size - 14u ;
1556+ block->_largest_unused_area = 5u ;
1557+ block->_search_start = 72u ;
1558+ block->_search_end = area_size - 13u ;
1559+ block->add_flags (JitAllocatorBlock::kFlagDirty );
1560+ block->clear_flags (JitAllocatorBlock::kFlagIncremental );
1561+
1562+ // Ask for 7 areas. With the bug, alloc picks the tail past _search_end and
1563+ // hands back a Span extending past the block end. With the fix, the search
1564+ // reports no fit and a fresh block is allocated.
1565+ JitAllocator::Span span;
1566+ EXPECT_EQ (allocator.alloc (Out (span), size_t (7u ) * granularity), Error::kOk );
1567+ EXPECT_NOT_NULL (span.rx ());
1568+
1569+ uint8_t * span_rx = static_cast <uint8_t *>(span.rx ());
1570+ uint8_t * span_end = span_rx + span.size ();
1571+ uint8_t * block_end = block->rx_ptr () + block->block_size ();
1572+
1573+ // With the fix, span lands in a fresh block and this guard is false; the
1574+ // primary signal is the absence of the in-block-bound assert in alloc.
1575+ // Kept as a guard against regressions that return the buggy in-block span.
1576+ if (span_rx >= block->rx_ptr () && span_rx < block_end) {
1577+ EXPECT_LE (span_end, block_end)
1578+ .message (" Span [%p:%p] extends past block end %p" , span_rx, span_end, block_end);
1579+ }
1580+ }
1581+
14531582static void test_jit_allocator_reset_empty () noexcept {
14541583 JitAllocator allocator;
14551584 allocator.reset (ResetPolicy::kSoft );
@@ -1635,6 +1764,8 @@ static void test_jit_allocator_query() noexcept {
16351764}
16361765
16371766UNIT (jit_allocator) {
1767+ test_bit_vector_range_iterator_bounds ();
1768+ test_jit_allocator_search_end_bounds ();
16381769 test_jit_allocator_reset_empty ();
16391770 test_jit_allocator_alloc_release ();
16401771 test_jit_allocator_query ();
0 commit comments