fix(ntx): reset emptied leaf to a pristine page layout on erase (PACK/reindex heap overrun)#85
fix(ntx): reset emptied leaf to a pristine page layout on erase (PACK/reindex heap overrun)#85Admnwk wants to merge 1 commit into
Conversation
NtxIndex::erase uses a swap-to-tail free-slot scheme that, once a leaf reaches key_count == 0, leaves offset[0] pointing at the last-removed key's physical slot rather than the pristine first slot. The next insert into that empty-but-rooted leaf (the PACK/reindex re-insert handled by the empty-leaf branch of seek_key_for_write_) takes offset[kc] as its free slot and marches the free pointer forward from there -- off the end of the 1024-byte page once enough keys are re-inserted, overrunning the fixed Page buffer (heap corruption). It surfaces as 6106 "short read on NTX page" and then a crash in flush() on AdsCloseTable. Reset the page to a pristine empty layout when the last key is removed so every free-slot offset is valid again. Add a multi-page regression test: the existing 4-record single-page test never grows past one leaf, so it cannot reach the overrun. (Full multi-page key reachability after reindex over clustered duplicate keys is a separate, pre-existing NTX limitation, not addressed here.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request fixes a heap-corruption crash during NTX PACK/reindex operations by resetting empty leaf pages to a pristine layout. It also introduces a new multi-page regression test. The review feedback recommends simplifying the key count check in ntx_index.cpp for better readability and adding a bounds check in the test file to prevent a potential buffer overflow when copying the temporary directory path.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| set_key_offset(leaf, kc, get_key_offset(leaf, kc)); // tail unchanged | ||
|
|
||
| set_key_count(leaf, kc - 1); | ||
| if (kc - 1 == 0) { |
| UNSIGNED8 srv[256]; | ||
| std::memcpy(srv, dir.string().c_str(), dir.string().size() + 1); |
There was a problem hiding this comment.
There is a potential buffer overflow here if the temporary directory path length exceeds 255 characters. Since srv has a fixed size of 256 bytes, copying dir.string().size() + 1 bytes without checking the length can lead to out-of-bounds memory writes and test crashes in environments with long paths.
We should add a safety check to ensure the path fits within the buffer.
| UNSIGNED8 srv[256]; | |
| std::memcpy(srv, dir.string().c_str(), dir.string().size() + 1); | |
| UNSIGNED8 srv[256]; | |
| REQUIRE(dir.string().size() < sizeof(srv)); | |
| std::memcpy(srv, dir.string().c_str(), dir.string().size() + 1); |
Problem
AdsPackTableon a DBF carrying a multi-page NTX index with deleted records can corrupt the heap and crash. PACK rebuilds the index viaTable::reindex(), which empties every index key-by-key and then re-inserts the survivors.NtxIndex::eraseuses a swap-to-tail free-slot scheme: when a key is removed, its slot offset is moved to the tail of the offset array. Once a leaf reacheskey_count == 0, that leavesoffset[0]pointing at the last-removed key's physical slot instead of the pristine first slot.The subsequent re-insert into that empty-but-rooted leaf (the empty-leaf branch of
seek_key_for_write_) takesoffset[kc]as its free slot and marches the free pointer forward from there. Once enough keys are re-inserted it walks off the end of the 1024-byte page, overrunning the fixedPagebuffer. That heap corruption surfaces first as6106 "short read on NTX page"(a child pointer reads as garbage) and then a crash insideflush()onAdsCloseTable.The existing
abi_ntx_pack_reindex_test(4 records, single leaf) never grows past one page, so it cannot reach the overrun.Fix
When
eraseremoves the last key from a leaf, reset that page to a pristine empty layout (format_empty_page) so every free-slot offset is valid again. 13 lines inntx_index.cpp.Test
Adds
abi_ntx_pack_reindex_multipage_test: 400 records (multi-page NTX), deletes a clustered block so reindex empties whole leaves, then PACKs.REQUIRE(AdsPackTable == 0)fails (returns 6106) and the process exits withSTATUS_HEAP_CORRUPTION(0xC0000374).Full suite green (785/785, MSVC x64 RelWithDebInfo).
Scope
This addresses the crash / heap overrun only. Full multi-page key reachability after reindex over clustered duplicate keys (Seek/Skip reaching every survivor) is a separate, pre-existing NTX limitation that is not addressed here and is left for a follow-up; the new test therefore asserts crash-safety and record count, not full key reachability.