Skip to content

Tree::~Tree() calls std::terminate when a node throws during halt (e.g. TryCatch catch_on_halt) #1281

Open

Description

@facontidavide

Tree::~Tree() calls haltTree() (src/bt_factory.cpp:719). A destructor is implicitly noexcept, so if any node throws while halting, the process aborts in std::terminate.

One easy way to get there is TryCatch catch_on_halt="true": TryCatchNode::halt() ticks the catch child, and a tick is allowed to throw. The tree-exec fuzzer in #1277 found this; it currently avoids the case by never generating catch_on_halt="true".

Reproducer (master, 1a21a78)

class Thrower : public BT::SyncActionNode {
public:
  Thrower(const std::string& n, const BT::NodeConfig& c) : SyncActionNode(n, c) {}
  static BT::PortsList providedPorts() { return {}; }
  BT::NodeStatus tick() override { throw std::runtime_error("cleanup failed"); }
};

// <TryCatch catch_on_halt="true"> <Sleep msec="1000"/> <Thrower/> </TryCatch>
auto tree = factory.createTreeFromText(xml);
tree.tickExactlyOnce();   // RUNNING
// tree goes out of scope
terminate called after throwing an instance of 'BT::NodeExecutionError'
  what():  Exception in node 'Thrower::3' [Thrower]: cleanup failed
Aborted (core dumped)

On master, catching doesn't help either. If the user calls tree.haltTree() inside a try/catch before the tree is destroyed, that first exception is caught, but ~Tree() still aborts. TryCatchNode::halt() throws before it reaches ControlNode::halt(), so the node stays RUNNING, and the destructor halts it a second time and throws again.

Relation to #1273

#1273 (reworked) adds a guard to TreeNode::haltNode(): when halt() throws, the node's status is reset to IDLE before the exception propagates. That fixes the second scenario (catch haltTree(), then destroy the tree). The first scenario still terminates, because there the first throw happens inside ~Tree() itself.

Possible fix

  • Don't let exceptions escape ~Tree(): catch around haltTree() and report them on stderr, as other destructors do.
  • TryCatchNode::halt() still skips resetting in_catch_/current_child_idx_ when the catch child throws. A try/catch + rethrow there would leave the node fully reset.
  • Once fixed, OSS-Fuzz: Add new fuzzer/corpus/dict targets tree exec #1277's fuzzer can generate catch_on_halt="true" again.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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