Skip to content

b+tree: three front-gap overruns — a node's entries do not start at zero - #169

Merged
psiha merged 1 commit into
wip/devector-livefrom
fix/160-append-and-free-overruns-the-front-gap
Sep 11, 2026
Merged

b+tree: three front-gap overruns — a node's entries do not start at zero#169
psiha merged 1 commit into
wip/devector-livefrom
fix/160-append-and-free-overruns-the-front-gap

Conversation

@psiha

@psiha psiha commented Sep 11, 2026

Copy link
Copy Markdown
Owner

On #167 (which is what makes wip/devector-live buildable off Windows at all).
Closes #160's devector faultbp_tree.nonunique and bp_tree.playground both
go from SIGSEGV to passing.

One mistake, three places

Each is: size the write from num_vals, then address through key_at, which is
keys[ start + i ] — so a node carrying a front gap is written past the end of its
array by exactly start.

append_and_free

BOOST_ASSUME( target.num_vals + source.num_vals <= target.max_values );
std::ranges::move( keys( source ), &key_at( target, target.num_vals ) );

Witnessed: start=29, num_vals=123, max_values=123 — 29 entries past the end.

relieve_into_sibling, give-to-left

left_room = max - p_left->num_vals ignores start, while that branch receives at
the sibling's tail. With max=123, num_vals=100, start=20 it hands over 11
entries into 3 free slots.

The give-to-right branch is already correct, for a different reason: it receives
at the front, where the gap is the supply, and it spends that before shifting
the deficit. So the two sides genuinely count room differently — the comment now says
so rather than leaving it to be rediscovered.

bp_tree_impl::merge — twice over

available_space is max - num_vals again, and the move_backward that opens
the merge point targets tgt_keys[ num_vals + copy_size ] where tgt_keys is
&keys[ start ]. Witnessed: start=1, num_vals=123, max_values=123.

recentre()

Closes the gap. Safe for inner nodes and cheap, because it preserves every logical
index — entry i sits at start + i before and at i after — so a child's
parent_child_idx still names the same child. (Contrast open_slot_from_front, which
is leaves-only for the opposite reason.)

Each necessary, none sufficient

nonunique playground
append_and_free only SIGSEGV SIGSEGV
+ left_room passes SIGSEGV
+ merge passes passes

Established in isolated clean builds, which is the only way it was decidable. Worth
recording why: testing one candidate while another site was still corrupting made it
look like a no-op — I wrote left_room off as refuted on exactly that basis — and
once a build EXIT=1 left a stale binary in place, a passing run looked like a fix.

How it was found, and the suggestion that falls out

Giving verify() a defaulted std::source_location so the first trap names its own
caller. That, plus trapping on start + num_vals > max_values, took it from "somewhere
in the bulk lanes" (the WIP probe commit's reading, which none of 14 placed probes
supported) to three named sites.

⭐ The reason it was hard: the gap-touching paths call verify_min_max(), which
checks num_vals against min/max and cannot see start + num_vals at all. A
violation created in one of them therefore surfaces far away, at whatever reader trips
over it next — keys(), insert's entry check, or update_separator. Teaching that
check the addressable-range invariant would localize this whole class at creation, and
is worth doing independently of these fixes.

Gate

Full suite, Release, Linux/macOS clang: 2049 passed / 1 pre-existing skip / 0 failed.

🤖 Generated with Claude Code

All three are one mistake in three places: a path sizes its write from
num_vals and then addresses through key_at, which is keys[ start + i ] -
so a node carrying a front gap is written past the end of its array by
exactly 'start'.  Together they close the devector fault: bp_tree.
nonunique and bp_tree.playground both go from SIGSEGV to passing.

append_and_free moves the source's entries to key_at( target, num_vals )
under a bound that only covers num_vals.  Witnessed with a trap on the
invariant: start=29, num_vals=123, max_values=123 - 29 entries past the
end.

relieve_into_sibling computes both siblings' room as max - num_vals,
while its give-to-LEFT branch receives at the sibling's TAIL, where a
front gap supplies nothing.  With max=123, num_vals=100 and start=20 it
hands over 11 entries into 3 free slots.  The give-to-RIGHT branch is
already correct for a different reason: it receives at the FRONT, where
the gap IS the supply, and spends it before shifting the deficit - so
the two sides genuinely count room differently, and the comment now
says so.

bp_tree_impl::merge has it twice over: available_space is again
max - num_vals, and the move_backward that opens the merge point targets
tgt_keys[ num_vals + copy_size ], where tgt_keys is &keys[ start ].
Witnessed: start=1, num_vals=123, max_values=123.

The new recentre() closes the gap.  It is safe for inner nodes and
cheap, because it preserves every logical index - entry i sits at
start + i before and at i after - so a child's parent_child_idx still
names the same child.

Each fix is necessary and none is sufficient: nonunique needs the first
two, playground needs the third as well.  Established in isolated clean
builds, which is the only way any of it was decidable - testing one
candidate while another site was still corrupting made it look like a
no-op, twice.

Found by giving verify() a defaulted std::source_location so the first
trap names its own caller.  Worth keeping in mind for the next one of
these: the gap-touching paths call verify_min_max(), which checks
num_vals against min/max and cannot see start + num_vals at all, so a
violation created in one of them surfaces far away at whatever reader
trips over it next.

Full suite: 2049 passed, 1 pre-existing skip, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@psiha
psiha force-pushed the fix/160-append-and-free-overruns-the-front-gap branch from 2b21db6 to 961894a Compare September 11, 2026 17:48
@psiha psiha changed the title b+tree: two front-gap overruns — a node's entries do not start at zero b+tree: three front-gap overruns — a node's entries do not start at zero Sep 11, 2026
Base automatically changed from fix/linux-dependent-base-qualification to wip/devector-live September 11, 2026 21:09
@psiha
psiha merged commit 961894a into wip/devector-live Sep 11, 2026
8 of 10 checks passed
@psiha
psiha deleted the fix/160-append-and-free-overruns-the-front-gap branch September 11, 2026 21: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