Skip to content

fix(ntx): reset emptied leaf to a pristine page layout on erase (PACK/reindex heap overrun)#85

Closed
Admnwk wants to merge 1 commit into
FiveTechSoft:mainfrom
Admnwk:fix/ntx-pack-segfault
Closed

fix(ntx): reset emptied leaf to a pristine page layout on erase (PACK/reindex heap overrun)#85
Admnwk wants to merge 1 commit into
FiveTechSoft:mainfrom
Admnwk:fix/ntx-pack-segfault

Conversation

@Admnwk

@Admnwk Admnwk commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Problem

AdsPackTable on a DBF carrying a multi-page NTX index with deleted records can corrupt the heap and crash. PACK rebuilds the index via Table::reindex(), which empties every index key-by-key and then re-inserts the survivors.

NtxIndex::erase uses 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 reaches key_count == 0, that leaves offset[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_) takes offset[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 fixed Page buffer. That heap corruption surfaces first as 6106 "short read on NTX page" (a child pointer reads as garbage) and then a crash inside flush() on AdsCloseTable.

The existing abi_ntx_pack_reindex_test (4 records, single leaf) never grows past one page, so it cannot reach the overrun.

Fix

When erase removes 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 in ntx_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.

  • Without the fix: REQUIRE(AdsPackTable == 0) fails (returns 6106) and the process exits with STATUS_HEAP_CORRUPTION (0xC0000374).
  • With the fix: PACK returns 0, record count is correct, and close no longer crashes.

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.

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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using kc == 1 is more direct and readable than kc - 1 == 0. It also avoids any potential mental overhead regarding unsigned underflow (even though kc is promoted to int here, keeping it simple is always preferred).

    if (kc == 1) {

Comment on lines +36 to +37
UNSIGNED8 srv[256];
std::memcpy(srv, dir.string().c_str(), dir.string().size() + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

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.

Suggested change
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);

@Admnwk

Admnwk commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Merged directly to main as 14c799a (cherry-picked onto current main 98b6b4a per maintainer request). Full unit suite 800/800, 0 failures, -Werror clean. Closing as merged — thanks!

@Admnwk Admnwk closed this Jun 26, 2026
@Admnwk Admnwk deleted the fix/ntx-pack-segfault branch June 26, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant