Fix use-after-free on Node's cached mutation iterator in write_state - #49
Open
kaspermunch wants to merge 1 commit into
Open
Fix use-after-free on Node's cached mutation iterator in write_state#49kaspermunch wants to merge 1 commit into
kaspermunch wants to merge 1 commit into
Conversation
Node caches an iterator `it` into its `mutation_sites` map to speed up
repeated positional lookups (get_state/move_iterator). write_state(pos, 0)
erased the element by key and only *then* tested `it->first == pos` to decide
whether to reset the cached iterator:
mutation_sites.erase(pos); // invalidates `it` if it pointed at pos
if (it->first == pos) { // <-- reads the just-freed tree node
it = mutation_sites.begin();
}
std::map::erase invalidates iterators to the erased element, so when `it`
pointed at `pos` the subsequent `it->first` dereferenced a freed red-black
tree node (use-after-free). The stale read usually did not equal `pos`, so
`it` was left dangling; a later get_state -> move_iterator -> next(it)/prev(it)
then walked a wild pointer and crashed with EXC_BAD_ACCESS / SIGSEGV.
Because the crash only surfaces once the freed slot is reused, it manifested
as intermittent heap corruption during MCMC sampling that scaled with ARG
size x iterations and moved around with parameters/seed, rather than at the
erase itself.
Fix: decide before erasing. If `it` points at the element being removed, use
the iterator-returning map::erase(it), which erases and repositions `it` to
the following element in one step so it is never left dangling; otherwise
erase by key and leave `it` untouched. The successor is always valid and
interior (the INT_MAX sentinel is never erased), matching the invariant that
move_iterator relies on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Node caches an iterator
itinto itsmutation_sitesmap to speed up repeated positional lookups (get_state/move_iterator). write_state(pos, 0) erased the element by key and only then testedit->first == posto decide whether to reset the cached iterator:std::map::erase invalidates iterators to the erased element, so when
itpointed atposthe subsequentit->firstdereferenced a freed red-black tree node (use-after-free). The stale read usually did not equalpos, soitwas left dangling; a later get_state -> move_iterator -> next(it)/prev(it) then walked a wild pointer and crashed with EXC_BAD_ACCESS / SIGSEGV.Because the crash only surfaces once the freed slot is reused, it manifested as intermittent heap corruption during MCMC sampling that scaled with ARG size x iterations and moved around with parameters/seed, rather than at the erase itself.
Fix: decide before erasing. If
itpoints at the element being removed, use the iterator-returning map::erase(it), which erases and repositionsitto the following element in one step so it is never left dangling; otherwise erase by key and leaveituntouched. The successor is always valid and interior (the INT_MAX sentinel is never erased), matching the invariant that move_iterator relies on.