Skip to content

Commit 110b1b1

Browse files
committed
fix(jit_allocator): bound BitVectorRangeIterator range_start to _end (#3)
1 parent 0bd5787 commit 110b1b1

1 file changed

Lines changed: 132 additions & 1 deletion

File tree

asmjit/core/jitallocator.cpp

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
14531582
static 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

16371766
UNIT(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

Comments
 (0)