Skip to content

fix double-pop crash in process_queued_events (issue #465)#670

Merged
kris-jusiak merged 1 commit into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-465-double-pop-process-queued-events
May 23, 2026
Merged

fix double-pop crash in process_queued_events (issue #465)#670
kris-jusiak merged 1 commit into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-465-double-pop-process-queued-events

Conversation

@PavelGuzenfeld
Copy link
Copy Markdown
Contributor

Problem

process_queued_events dispatched 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 inner process_queued_events saw 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):

  1. Action calls back::process(e2{}) → e2 pushed to process_
  2. process_queued_events enters the while loop; front = e2 (not yet popped)
  3. Dispatches e2's action; the action calls sm.process_event(e3) re-entrantly
  4. The inner process_queued_events sees e2 at front again → dispatches e2 a second time, then pops it
  5. Outer loop calls process_.pop() on the now-empty queue → UB / crash

Related issues: #395 (same root in ASIO context), #552 (thread-safety manifestation).

Fixes

1. Pop before dispatch — resolves #465

Move the front queue_event into a local variable, pop the (now moved-from) element, then invoke the dispatch:

typename process_t::value_type event{static_cast<typename process_t::value_type&&>(process_.front())};
process_.pop();  // pop before dispatch so re-entrant calls see an advanced queue
queued_handled &= (this->*dispatch_table[event.id])(deps, subs, event.data);

Re-entrant calls see an already-advanced queue and cannot observe the in-flight event.

2. Null other.dtor after move in queue_eventresolves #279

The queue_event move constructor and move-assign transferred ownership of the payload but left other.dtor set.
Destroying the moved-from queue_event (as pop() does in fix 1) then 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 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_event to test/ft/actions_process.cpp:

  • SM transitions s1 --e1--> s2 with process(e2{}) as action
  • s2 --e2 action increments a counter and calls sm.process_event(e3) re-entrantly (e3 has no handler)
  • After sm.process_event(e1), asserts the counter is 1 (not 2), and the SM is in state X

With 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)

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.
@kris-jusiak kris-jusiak merged commit 0672b84 into boost-ext:master May 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants