Skip to content

bump iterator state before pushing on sexp stack - #1913

Merged
lionel- merged 1 commit into
r-lib:mainfrom
kevinushey:fix-walk-stale-pointer
Aug 5, 2026
Merged

bump iterator state before pushing on sexp stack#1913
lionel- merged 1 commit into
r-lib:mainfrom
kevinushey:fix-walk-stale-pointer

Conversation

@kevinushey

Copy link
Copy Markdown
Contributor

Fixes #1911.

sexp_next_incoming() (src/rlang/walk.c) receives p_info as a raw pointer into the traversal stack's dyn-array buffer. The child push could resize the stack — reallocating the buffer and dropping the old one's protection — after which the parent's state bump and incoming→outgoing flip were written through the stale pointer into the dead buffer. The live (copied) entry kept its pre-bump state, so the iterator revisited the same edge and re-traversed entire subtrees once depth exceeded the initial stack capacity of 256. It was also a latent use-after-free write, benign today only because no allocation happens between the resize and the writes.

This PR moves the state bump before the push. All reads of p_info used to build child happen earlier, so the reorder is behavior-preserving apart from the fix.

Since this code is only compiled under RLANG_USE_PRIVATE_ACCESSORS (and currently doesn't build on R >= 4.5, see the issue), there's no regression test in the default configuration. Verified in a flagged dev build with accessor stubs: before the fix, a depth-300 nested list yields 690 sexp_iterate() callback visits with 44 nodes visited twice as incoming; after the fix, exactly 601 visits (2n + 1) with no duplicates, and the depth-250 control is unchanged.

`sexp_next_incoming()` received `p_info` as a raw pointer into the
traversal stack's buffer. Pushing the child could resize the stack,
reallocating the buffer, after which the parent's state bump and
incoming/outgoing flip were written into the dead pre-resize buffer.
When the parent resurfaced, the stale copied state made the iterator
revisit the same edge, duplicating traversal of entire subtrees once
depth exceeded the initial stack capacity of 256.

Bump the state before the push so the updated state is what gets
copied on resize.

Fixes r-lib#1911.
@lionel-
lionel- merged commit eabe3b6 into r-lib:main Aug 5, 2026
13 checks passed
@lionel-

lionel- commented Aug 5, 2026

Copy link
Copy Markdown
Member

Thanks!

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.

sexp iterator revisits nodes after stack resize (stale pointer)

2 participants