Skip to content

fix(evolution): 🐛 bound the epoch-stamp mark by combined_size - #267

Draft
diagonal-hamiltonian wants to merge 2 commits into
mainfrom
fix/epoch-stamp-bounds-guard
Draft

fix(evolution): 🐛 bound the epoch-stamp mark by combined_size#267
diagonal-hamiltonian wants to merge 2 commits into
mainfrom
fix/epoch-stamp-bounds-guard

Conversation

@diagonal-hamiltonian

@diagonal-hamiltonian diagonal-hamiltonian commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

Summary

Bounds the epoch-stamp mark in resolve_self_queries by combined_size so it can no longer write
past the end of epoch_. fix/epoch-stamp-bounds-guard 92e352b, 1 commit on e0c528e, 2 files
+69/−2.

  1. The guard. resolve_self_queries marks under found[j] < op_size — the store size read at
    resolve_range_ entry — but epoch_ is sized to combined_size, the store size at engine
    construction. Once the store has grown, op_size > combined_size and the mark writes past the
    end of epoch_, into vector slack. Silently. This matches what the cross-rank twin
    resolve_incoming (Resolve.h:153) already does.
  2. Latent, not live. The only marking pass is is_leader_pass, and run_exchange(true, …)
    resolves self queries before both insert paths, so today the two sizes coincide. Reordering the
    passes breaks it. Pre-dates perf(evolution): ⚡ halve the follower-marking epoch stamp #259 and is kept out of it deliberately.
  3. Dropping such a mark is result-neutral, not merely in-bounds. Both is_marked readers
    (:406, :482) read scan-source indices out of fused, built at :562-565 before the engine
    is constructed at :592-598 — so every index reaching is_marked is < combined_size, and a
    mark at or above it is unreadable.
  4. The test constructs the out-of-bounds ordering rather than asserting today's accident.
    begin_gate(6) then combined_size=4, so query 2 resolves to found=5 against op_size=6 and
    the unfixed code writes epoch_[5]. Mutation-verified — reverting the guard fails the case
    deterministically.

Changes

  • Bound the epoch-stamp mark in resolve_self_queries by combined_size, matching
    resolve_incoming's existing guard.
  • No behavior change today: the only marking pass, is_leader_pass, never presents
    op_size > combined_size; the guard protects against reordering the passes, not against a live
    bug.
  • Add a regression test that constructs the out-of-bounds ordering directly (begin_gate(6),
    combined_size=4) instead of relying on today's coincidental sizing.

Measurement

No campaign and no table here: the only marking pass is is_leader_pass, once per pass, so one
added comparison on that path cannot move a timing. Gates: ctest -L unit 217/217, -L serial
216/216.

Relationship to the open PRs

Independent of #259 and #263 — no stacking needed. Pre-dates #259 and is deliberately kept out of it
so the stamp-narrowing story there stays one mechanism. Verified per-file against both heads: one
conflict hunk against each, in cpp/tests/evolution_detail_tests.cpp only, from adjacent test
insertions. Engine.h itself merges clean against #263 even though that PR edits the same file —
different regions.

Checklist

  • Tests added or updated to cover the changes
  • Documentation updated (docstrings, docs/, CONTRIBUTING.md) if needed
  • CHANGELOG / release notes updated if applicable

AI/LLM disclosure

  • I did not use LLM tooling, or used it only privately for ideation
  • I used the following tool to help write this PR description: Claude Code
  • I used the following tool to generate or modify code: Claude Code

Important

By opening this PR I confirm that I have read CONTRIBUTING.md and I agree to the terms of the Contributor License Agreement.

Warning

If you're contributing on behalf of your employer, contact cla@algorithmiq.fi to arrange a Corporate CLA.

resolve_self_queries marked under `found[j] < op_size`, where op_size is the store size read at
resolve_range_ entry, but epoch_ is sized to combined_size -- the store size at engine construction.
Once the store has grown, op_size > combined_size and the mark writes past the end of epoch_, into
vector slack, which is why nothing caught it.

Guard on combined_size, matching the cross-rank twin resolve_incoming (Resolve.h:153).

Latent, not live: the sole marking pass is is_leader_pass, and run_exchange(true, ...) resolves self
queries before both insert paths, so today the two sizes coincide. Reordering the passes breaks it.

Dropping such a mark is result-neutral, not merely in-bounds. Both is_marked readers (:406, :482)
read scan-source indices out of `fused`, built at :562-565 before the engine is constructed at
:592-598, so every index reaching is_marked is < combined_size. A mark at or above it is unreadable.

The test constructs the out-of-bounds ordering rather than asserting the accident: begin_gate(6) then
combined_size=4, so query 2 resolves to found=5 against op_size=6 and the unfixed code writes
epoch_[5]. Verified by mutation -- reverting the guard fails the case deterministically.
@github-actions github-actions Bot added the cpp label Aug 21, 2026
@github-actions

Copy link
Copy Markdown

Docs preview: https://pr-267.monoprop-docs.pages.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant