fix double-pop crash in process_queued_events (issue #465)#670
Merged
kris-jusiak merged 1 commit intoMay 23, 2026
Conversation
process_queued_events dispatched the front event before removing it from the queue. If an action called sm.process_event() re-entrantly, the inner call saw the same event still at the front, processed it a second time, and popped it. When control returned to the outer loop it called pop() on an empty queue — undefined behaviour / crash. Two related fixes: 1. Pop before dispatch (issue boost-ext#465): Move the front queue_event into a local variable, pop the (now moved-from) element, then invoke the dispatch. Re-entrant calls therefore see an already-advanced queue and cannot observe the in-flight event. 2. Null out other.dtor after move (issue boost-ext#279): The queue_event move-constructor and move-assign left other.dtor set after transferring ownership, so destroying the moved-from object called dtor() on already-moved-from storage — double-destruction. Zeroing other.dtor after the move makes the moved-from destructor a no-op, which is required for fix (1) to be sound for arbitrary event types. Regression test added to test/ft/actions_process.cpp: a SM whose e2 action calls sm.process_event(e3) re-entrantly; verified the e2 action fires exactly once (not twice) and the SM ends in the expected state.
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.
Problem
process_queued_eventsdispatched the front event before removing it from the queue.If an action called
sm.process_event()re-entrantly (e.g., from an ASIO callback or any other re-entrant call), the innerprocess_queued_eventssaw the same event still at the front, processed it a second time, then popped it.When control returned to the outer loop, it called
pop()on an already-empty queue — undefined behaviour / crash.Bug trace (re-entrant case):
back::process(e2{})→ e2 pushed toprocess_process_queued_eventsenters the while loop; front = e2 (not yet popped)sm.process_event(e3)re-entrantlyprocess_queued_eventssees e2 at front again → dispatches e2 a second time, then pops itprocess_.pop()on the now-empty queue → UB / crashRelated issues: #395 (same root in ASIO context), #552 (thread-safety manifestation).
Fixes
1. Pop before dispatch — resolves #465
Move the front
queue_eventinto a local variable, pop the (now moved-from) element, then invoke the dispatch:Re-entrant calls see an already-advanced queue and cannot observe the in-flight event.
2. Null
other.dtorafter move inqueue_event— resolves #279The
queue_eventmove constructor and move-assign transferred ownership of the payload but leftother.dtorset.Destroying the moved-from
queue_event(aspop()does in fix 1) then calleddtor()on already-moved-from storage — double-destruction.Zeroing
other.dtorafter the move makes the moved-from destructor a no-op, which is required for fix 1 to be sound for non-trivially-destructible event types.These two fixes are co-located because fix 2 is a soundness prerequisite for fix 1.
Regression test
Added
process_queue_no_double_pop_on_reentrant_process_eventtotest/ft/actions_process.cpp:s1 --e1--> s2withprocess(e2{})as actions2 --e2action increments a counter and callssm.process_event(e3)re-entrantly (e3 has no handler)sm.process_event(e1), asserts the counter is 1 (not 2), and the SM is in stateXWith the bug: counter == 2 and
pop()on empty queue is UB.With the fix: counter == 1, SM ends in
X, no UB.Closes #465
Also fixes #279 (move semantics)