Skip to content

Infinite recursion in do_handle_deferred with orthogonal regions (regression from 1.78) #184

@nikkikom

Description

@nikkikom

Infinite recursion in do_handle_deferred with orthogonal regions (regression from 1.78)

Environment

  • Boost version: 1.86.0 and later (works correctly in 1.78.0)
  • Compiler: GCC 14.2.0 (also reproducible with Clang 18)
  • OS: Linux x86_64

Description

do_handle_deferred enters infinite recursion when a state machine has orthogonal
regions and one region defers an event while another region handles the same event
with an internal transition. This causes a stack overflow / segfault.

The bug is a regression introduced between Boost 1.78 and 1.86 in the "retry on success" logic added to do_handle_deferred in boost/msm/back/state_machine.hpp.

Minimal reproducer

#include <boost/msm/back/state_machine.hpp>
#include <boost/msm/front/functor_row.hpp>
#include <boost/msm/front/state_machine_def.hpp>
#include <cstdio>

namespace msm = boost::msm;
namespace mpl = boost::mpl;
using namespace msm::front;

struct Ping {};

struct Machine_ : state_machine_def<Machine_>
{
    typedef int activate_deferred_events;

    struct Waiting : state<> {};
    struct Ready   : state<> {};
    struct Sink    : state<> {};

    // Two orthogonal regions
    typedef mpl::vector<Waiting, Sink> initial_state;

    struct transition_table : mpl::vector<
        //        Source   Event  Target  Action  Guard
        Row<      Waiting, Ping,  none,   Defer,  none>,  // region 1: defer
        Row<      Sink,    Ping,  none,   none,   none>   // region 2: handle (discard)
    > {};

    template <class FSM, class Event>
    void no_transition(Event const&, FSM&, int) {}
};

typedef msm::back::state_machine<Machine_> Machine;

int main()
{
    Machine sm;
    sm.start();
    std::puts("Processing Ping...");
    sm.process_event(Ping{});   // <-- stack overflow here
    std::puts("Done.");
}
  • Boost 1.78: prints "Done." and exits normally.
  • Boost 1.86: segfaults (stack overflow from ~30,000 recursive frames in do_handle_deferred).

Root cause

In Boost 1.78, do_handle_deferred simply iterates through the deferred queue and fires each event. Re-deferred events get a higher sequence number and are naturally skipped by the cur_seq != pair.second check:

// Boost 1.78 — state_machine.hpp:1891
void do_handle_deferred(bool new_seq=false)
{    
    if (new_seq) ++m_events_queue.m_cur_seq;    
    char& cur_seq = m_events_queue.m_cur_seq;
    while (!m_events_queue.m_deferred_events_queue.empty())
    {
        auto& pair = m_events_queue.m_deferred_events_queue.front();
        if (cur_seq != pair.second) break;
        deferred_fct next = pair.first;
        m_events_queue.m_deferred_events_queue.pop_front();
        next();   // return value ignored — safe
    }
}

Boost 1.86 added logic to check the return value and recursively retry when an event was "successfully processed":

// Boost 1.86 — state_machine.hpp:1971
void do_handle_deferred(bool new_seq=false)
{
    if (new_seq) ++m_events_queue.m_cur_seq;
    char& cur_seq = m_events_queue.m_cur_seq;
    
    bool not_only_deferred = false;
    while (!m_events_queue.m_deferred_events_queue.empty())
    {
        auto& pair = m_events_queue.m_deferred_events_queue.front();
        if (cur_seq != pair.second) break;

        deferred_fct next = pair.first;
        m_events_queue.m_deferred_events_queue.pop_front(); 
        boost::msm::back::execute_return res = next(); 
        
        if (res != HANDLED_FALSE && res != HANDLED_DEFERRED)  // <-- BUG
            not_only_deferred = true;
        if (not_only_deferred)
            break;
    }
    if (not_only_deferred)    {
        std::stable_sort(/* ... */);
        std::for_each(/* ... */, set_sequence(m_events_queue.m_cur_seq + 1));
        do_handle_deferred(true);   // <-- infinite recursion
    }
}

The check on line 2001 uses equality:

res != HANDLED_DEFERRED

With orthogonal regions, region_processing_helper::In::process OR's the result from each region (line 2140):

result_ = (HandledEnum)((int)result_ | (int)res);

When region 1 defers (HANDLED_DEFERRED = 2) and region 2 handles (HANDLED_TRUE = 1), the combined result is HANDLED_TRUE | HANDLED_DEFERRED = 3. The equality check 3 != HANDLED_DEFERRED(2) evaluates to true, so the code incorrectly concludes the event was successfully processed. It resets the sequence numbers of remaining deferred events and calls do_handle_deferred(true) recursively. The recursive call increments cur_seq to match the re-deferred event, replays it, gets the same OR'd result, and recurses again - infinitely.

Suggested fix

Change the equality check to a bitmask check on line 2001 of boost/msm/back/state_machine.hpp:

// Before (broken for multi-region OR'd results):
if (res != ::boost::msm::back::HANDLED_FALSE && res != ::boost::msm::back::HANDLED_DEFERRED)
// After (correctly detects HANDLED_DEFERRED bit in OR'd results):
if (res != ::boost::msm::back::HANDLED_FALSE && !(res & ::boost::msm::back::HANDLED_DEFERRED))

This correctly identifies that the event was re-deferred (even when another region also handled it), preventing the recursive retry.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions