This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch fix-aio-callback-lifetime
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit b7fca54a285bbbe05037414c9e9a9205c0507bb1
Author: bneradt <[email protected]>
AuthorDate: Fri May 8 17:03:53 2026 -0500

    Fix AIO callback completion lifetime
    
    Dirty cache recovery can complete an AIO operation by invoking a state
    machine that releases the temporary recovery state owning the callback.
    After the API AIO cleanup, the generic completion path read the
    callback again after invoking that continuation, which let ASan abort
    during startup recovery and restart Traffic Server in a loop.
    
    This snapshots the API-owned callback flag before dispatching the
    completion and uses that local value for the post-callback cleanup.
    This also adds a focused regression test for completion handlers that
    release the callback owner before AIOCallback::io_complete() returns.
---
 src/iocore/aio/AIO.cc                         |  5 +-
 src/iocore/aio/CMakeLists.txt                 |  4 ++
 src/iocore/aio/unit_tests/test_AIOCallback.cc | 67 +++++++++++++++++++++++++++
 3 files changed, 75 insertions(+), 1 deletion(-)

diff --git a/src/iocore/aio/AIO.cc b/src/iocore/aio/AIO.cc
index f8d63d3f3b..98d84cdd93 100644
--- a/src/iocore/aio/AIO.cc
+++ b/src/iocore/aio/AIO.cc
@@ -85,6 +85,9 @@ AIOCallback::io_complete(int event, void *data)
 {
   (void)event;
   (void)data;
+  // The completion continuation can release the object that owns this 
callback.
+  bool const self_delete = from_api;
+
   if (aio_err_callback && !ok()) {
     AIOCallback *err_op          = new AIOCallback();
     err_op->aiocb.aio_fildes     = this->aiocb.aio_fildes;
@@ -99,7 +102,7 @@ AIOCallback::io_complete(int event, void *data)
   if (!action.cancelled && action.continuation) {
     action.continuation->handleEvent(AIO_EVENT_DONE, this);
   }
-  if (from_api) {
+  if (self_delete) {
     delete this;
   }
   return EVENT_DONE;
diff --git a/src/iocore/aio/CMakeLists.txt b/src/iocore/aio/CMakeLists.txt
index 3d06b67ae1..8040a31118 100644
--- a/src/iocore/aio/CMakeLists.txt
+++ b/src/iocore/aio/CMakeLists.txt
@@ -33,6 +33,10 @@ if(BUILD_TESTING)
     COMMAND $<TARGET_FILE:test_AIO>
     WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/iocore/aio
   )
+
+  add_executable(test_AIOCallback unit_tests/test_AIOCallback.cc)
+  target_link_libraries(test_AIOCallback PRIVATE ts::aio 
Catch2::Catch2WithMain)
+  add_catch2_test(NAME test_AIOCallback COMMAND test_AIOCallback)
 endif()
 
 clang_tidy_check(aio)
diff --git a/src/iocore/aio/unit_tests/test_AIOCallback.cc 
b/src/iocore/aio/unit_tests/test_AIOCallback.cc
new file mode 100644
index 0000000000..0fcd131309
--- /dev/null
+++ b/src/iocore/aio/unit_tests/test_AIOCallback.cc
@@ -0,0 +1,67 @@
+/** @file
+
+  Catch based unit tests for AIOCallback.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "iocore/aio/AIO.h"
+#include "iocore/eventsystem/Event.h"
+
+namespace
+{
+
+struct AIOCompletionOwner : Continuation {
+  AIOCallback callback;
+  bool       *completed = nullptr;
+
+  explicit AIOCompletionOwner(bool &completion_flag) : Continuation(nullptr), 
completed(&completion_flag)
+  {
+    SET_HANDLER(&AIOCompletionOwner::handle_aio_complete);
+
+    callback.action           = this;
+    callback.aiocb.aio_nbytes = 0;
+    callback.aio_result       = 0;
+  }
+
+  int
+  handle_aio_complete(int event, void *data)
+  {
+    CHECK(event == AIO_EVENT_DONE);
+    CHECK(data == &callback);
+
+    *completed = true;
+    delete this;
+    return EVENT_DONE;
+  }
+};
+
+} // namespace
+
+TEST_CASE("AIOCallback completion tolerates owner deletion", "[iocore][aio]")
+{
+  bool completed = false;
+  auto owner     = new AIOCompletionOwner(completed);
+  auto callback  = &owner->callback;
+
+  CHECK(callback->io_complete(EVENT_NONE, nullptr) == EVENT_DONE);
+  CHECK(completed);
+}

Reply via email to