Repository: actor-framework/actor-framework (2026-03-21T09:22:42Z)

Issue:
Calling `quit` in an actor shell trigges an assertion
We found a nasty bug in our actor shell implementation. The original setup involved sending requests from an actor shell and the program would die in a CAF assertion if that timed out. However, the root cause was simply that the error handler for the timeout calls `quit` and the actor shell then continues to try read more messages from the closed mailbox.


Discussion:

Patch:
diff --git a/libcaf_net/caf/net/abstract_actor_shell.cpp b/libcaf_net/caf/net/abstract_actor_shell.cpp
index 22cbf81bec..44cfeea3fe 100644
--- a/libcaf_net/caf/net/abstract_actor_shell.cpp
+++ b/libcaf_net/caf/net/abstract_actor_shell.cpp
@@ -27,8 +27,13 @@ abstract_actor_shell::abstract_actor_shell(actor_config& cfg,
   mailbox_.try_block();
   resume_ = make_action([this] {
     for (;;) {
-      if (!consume_message() && try_block_mailbox())
+      auto consumed = consume_message();
+      if (terminated()) { // Stop unconditionally if the actor called `quit()`.
         return;
+      }
+      if (!consumed && try_block_mailbox()) {
+        return;
+      }
     }
   });
 }
@@ -52,9 +57,9 @@ void abstract_actor_shell::quit(error reason) {
 // -- mailbox access -----------------------------------------------------------
 
 mailbox_element_ptr abstract_actor_shell::next_message() {
-  if (!mailbox_.blocked())
-    return mailbox_.pop_front();
-  return nullptr;
+  if (mailbox_.closed() || mailbox_.blocked())
+    return nullptr;
+  return mailbox_.pop_front();
 }
 
 bool abstract_actor_shell::try_block_mailbox() {


Test patch:
diff --git a/libcaf_net/caf/net/actor_shell.test.cpp b/libcaf_net/caf/net/actor_shell.test.cpp
index 6270752cfc..1d4c8368ac 100644
--- a/libcaf_net/caf/net/actor_shell.test.cpp
+++ b/libcaf_net/caf/net/actor_shell.test.cpp
@@ -104,6 +104,44 @@ class app_t : public net::octet_stream::upper_layer {
   net::actor_shell_ptr self;
 };
 
+/// Sends a message to itself on startup and then calls `quit()` from the
+/// message handler.
+struct quit_from_handler_app final : net::octet_stream::upper_layer {
+  explicit quit_from_handler_app(actor observer) : observer_(observer) {
+    // nop
+  }
+
+  error start(net::octet_stream::lower_layer* down) override {
+    self_ = net::make_actor_shell(down->manager());
+    self_->set_behavior([this](const std::string&) {
+      self_->quit(make_error(exit_reason::user_shutdown));
+    });
+    down->configure_read(net::receive_policy::up_to(2048));
+    self_->mail(actor_cast<actor>(self_->ctrl())).send(observer_);
+    return none;
+  }
+
+  void prepare_send() override {
+  }
+
+  bool done_sending() override {
+    return true;
+  }
+
+  void abort(const error&) override {
+    // nop
+  }
+
+  ptrdiff_t consume(byte_span buf, byte_span) override {
+    return static_cast<ptrdiff_t>(buf.size());
+  }
+
+private:
+  actor observer_;
+
+  net::actor_shell_ptr self_;
+};
+
 actor_system_config& init(actor_system_config& cfg) {
   cfg.set("caf.scheduler.max-threads", 2);
   cfg.load<net::middleman>();
@@ -235,4 +273,26 @@ TEST("actor shells can use flows") {
   check_eq(to_string_view(buf), msg);
 }
 
+TEST("GH-2299 regression") {
+  scoped_actor self{sys};
+  auto& mpx = sys.network_manager().mpx();
+  auto app = std::make_unique<quit_from_handler_app>(actor{self});
+  auto transport = net::octet_stream::transport::make(fd1, std::move(app));
+  auto mgr = net::socket_manager::make(&mpx, std::move(transport));
+  require(mpx.start(mgr));
+  fd1.id = net::invalid_socket_id;
+  actor uut;
+  self->receive([&uut](actor& x) { uut = x; },
+                after(1s) >> [this] { fail("shell did not send its handle"); });
+  self->monitor(uut);
+  self->mail("bye 1"s).send(uut);
+  self->mail("bye 2"s).send(uut);
+  self->receive(
+    [this, uut](const down_msg& msg) {
+      check_eq(msg.source, uut);
+      check_eq(msg.reason, exit_reason::user_shutdown);
+    },
+    after(1s) >> [this] { fail("shell did not exit"); });
+}
+
 } // WITH_FIXTURE(fixture)