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

xiaoxiang781216 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit 72c45233a0e5135e981307a7a798acbeca48f51d
Author: ouyangxiangzhen <[email protected]>
AuthorDate: Thu Nov 12 00:47:34 2026 +0800

    fs/aio: cancel outstanding requests when the process exits
    
    A request still queued on the low priority worker when its process
    exits keeps a container that points at the process's aiocb, which
    usually lives on a stack that is about to be freed.  The worker later
    runs the request and writes aio_result and lio_link into that memory.
    
    POSIX cancels the outstanding requests of a process that terminates.
    Add aio_cleanup(), called from nxtask_exithook() before group_leave():
    for the exiting task it takes every container of its process off the
    pending list, cancels the queued work, waits for one already running,
    and frees the container.  aioc_pid holds the process ID, so this only
    matches when the main thread exits.  aioc_decant() leaves a container
    marked this way alone.
    
    Signed-off-by: ouyangxiangzhen <[email protected]>
    Signed-off-by: jingfei <[email protected]>
---
 fs/aio/aio.h               |  6 +++-
 fs/aio/aio_cancel.c        | 79 ++++++++++++++++++++++++++++++++++++++++++++++
 fs/aio/aioc_contain.c      | 30 +++++++++++++-----
 include/aio.h              | 30 ++++++++++++++++++
 sched/task/task_exithook.c |  7 ++++
 5 files changed, 143 insertions(+), 9 deletions(-)

diff --git a/fs/aio/aio.h b/fs/aio/aio.h
index 769cd7015eb..aeb63ef845b 100644
--- a/fs/aio/aio.h
+++ b/fs/aio/aio.h
@@ -64,7 +64,11 @@ struct aio_container_s
   FAR struct aiocb *aioc_aiocbp;   /* The contained AIO control block */
   FAR struct file *aioc_filep;     /* File structure to use with the I/O */
   struct work_s aioc_work;         /* Used to defer I/O to the work thread */
-  pid_t aioc_pid;                  /* ID of the waiting task */
+  pid_t aioc_pid;                  /* ID of the waiting task.  Set to
+                                    * INVALID_PROCESS_ID once aio_cleanup()
+                                    * has taken ownership of the container;
+                                    * aioc_decant() then leaves it alone.
+                                    */
 #ifdef CONFIG_PRIORITY_INHERITANCE
   uint8_t aioc_prio;               /* Priority of the waiting task */
 #endif
diff --git a/fs/aio/aio_cancel.c b/fs/aio/aio_cancel.c
index ca26f5a1d66..bff7a75fb17 100644
--- a/fs/aio/aio_cancel.c
+++ b/fs/aio/aio_cancel.c
@@ -32,6 +32,7 @@
 #include <errno.h>
 
 #include <nuttx/wqueue.h>
+#include <nuttx/sched.h>
 #include <nuttx/fs/fs.h>
 
 #include "aio/aio.h"
@@ -229,4 +230,82 @@ int aio_cancel(int fildes, FAR struct aiocb *aiocbp)
   return ret;
 }
 
+/****************************************************************************
+ * Name: aio_cleanup
+ *
+ * Description:
+ *   Cancel all pending AIO operations submitted by task 'pid'.  Called
+ *   from the task exit path so a dying task does not leave pending
+ *   containers referencing its soon-to-be-freed aiocb (which usually
+ *   resides on the task's stack).  Queued operations are cancelled before
+ *   they can run; an operation already running on the worker thread is
+ *   waited for, because the worker dereferences the user aiocb (aio_result,
+ *   aio_signal()) until its callback returns.  Ownership of each container
+ *   is taken by setting aioc_pid to INVALID_PROCESS_ID (so aioc_decant()
+ *   will not free it) before the synchronized cancel; the container is
+ *   released here once the worker thread is done.  No completion signal is
+ *   delivered: the owning task is gone and signalling it would
+ *   dereference the very aiocb this cleanup protects.
+ *
+ * Input Parameters:
+ *   pid - ID of the exiting task
+ *
+ * Returned Value:
+ *   None
+ *
+ ****************************************************************************/
+
+void aio_cleanup(pid_t pid)
+{
+  FAR struct aio_container_s *aioc;
+  FAR struct aio_container_s *next;
+  dq_queue_t queue;
+
+  dq_init(&queue);
+
+  /* Take ownership of every container submitted by the exiting task: mark
+   * it so aioc_decant() will not touch it, detach it from the pending list
+   * and remember it for the synchronized cancel below.  The container must
+   * stay valid while the worker thread may still be running the operation.
+   */
+
+  aio_lock();
+
+  for (aioc = (FAR struct aio_container_s *)g_aio_pending.head;
+       aioc;
+       aioc = next)
+    {
+      next = (FAR struct aio_container_s *)aioc->aioc_link.flink;
+
+      if (aioc->aioc_pid != pid)
+        {
+          continue;
+        }
+
+      aioc->aioc_pid = INVALID_PROCESS_ID;
+      dq_rem(&aioc->aioc_link, &g_aio_pending);
+      dq_addlast(&aioc->aioc_link, &queue);
+    }
+
+  aio_unlock();
+
+  /* Cancel the operations that are still queued and wait for any operation
+   * already running on the worker thread.  This must run outside of the aio
+   * critical section: the worker takes aio_lock to release its container,
+   * so waiting here with the lock held would deadlock.  Once
+   * work_cancel_sync() returns, the worker is done touching the user aiocb
+   * and the container, so the task stack may be freed safely.  The
+   * container is released manually: aioc_decant() would refuse it because
+   * of the ownership marker.
+   */
+
+  while ((aioc = (FAR struct aio_container_s *)
+          dq_remfirst(&queue)) != NULL)
+    {
+      work_cancel_sync(LPWORK, &aioc->aioc_work);
+      file_put(aioc->aioc_filep);
+      aioc_free(aioc);
+    }
+}
+
 #endif /* CONFIG_FS_AIO */
diff --git a/fs/aio/aioc_contain.c b/fs/aio/aioc_contain.c
index 55447972ca9..0efe46d7977 100644
--- a/fs/aio/aioc_contain.c
+++ b/fs/aio/aioc_contain.c
@@ -128,11 +128,17 @@ errout:
  *   Remove the AIO control block from the container and free all resources
  *   used by the container.
  *
+ *   If aioc_pid is INVALID_PROCESS_ID, ownership of the container has been
+ *   transferred to aio_cleanup() and the container is left untouched:
+ *   it has already been detached from the pending list and the task exit
+ *   path releases it once the worker thread has finished.
+ *
  * Input Parameters:
  *   aioc - Pointer to the AIO control block container
  *
  * Returned Value:
- *   A pointer to the no-longer contained AIO control block.
+ *   A pointer to the no-longer contained AIO control block.  NULL if the
+ *   container is owned by the task exit path and was not de-canted.
  *
  ****************************************************************************/
 
@@ -148,15 +154,23 @@ FAR struct aiocb *aioc_decant(FAR struct aio_container_s 
*aioc)
   ret = aio_lock();
   if (ret >= 0)
     {
-      dq_rem(&aioc->aioc_link, &g_aio_pending);
-
-      /* De-cant the AIO control block and return the container to the
-       * free list.
+      /* An INVALID_PROCESS_ID marks a container owned by aio_cleanup():
+       * it is no longer in the pending list and freeing it here would race
+       * with (or double-free against) the task exit path.  Leave it alone.
        */
 
-      aiocbp = aioc->aioc_aiocbp;
-      file_put(aioc->aioc_filep);
-      aioc_free(aioc);
+      if (aioc->aioc_pid != INVALID_PROCESS_ID)
+        {
+          dq_rem(&aioc->aioc_link, &g_aio_pending);
+
+          /* De-cant the AIO control block and return the container to the
+           * free list.
+           */
+
+          aiocbp = aioc->aioc_aiocbp;
+          file_put(aioc->aioc_filep);
+          aioc_free(aioc);
+        }
 
       aio_unlock();
     }
diff --git a/include/aio.h b/include/aio.h
index 97a3654fab5..69289ff4ac1 100644
--- a/include/aio.h
+++ b/include/aio.h
@@ -48,6 +48,36 @@
 #  undef CONFIG_FS_AIO
 #endif
 
+/****************************************************************************
+ * Name: aio_cleanup
+ *
+ * Description:
+ *   Cancel all pending AIO operations submitted by task 'pid'.  Called
+ *   from the task exit path so a dying task does not leave pending
+ *   containers referencing its soon-to-be-freed aiocb (which usually
+ *   resides on the task's stack).  Queued operations are cancelled before
+ *   they can run; an operation already running on the worker thread is
+ *   waited for, because the worker dereferences the user aiocb until its
+ *   callback returns.  No completion signal is delivered: the owning task
+ *   is gone and signalling it would dereference the very aiocb this
+ *   cleanup protects.
+ *
+ *   When CONFIG_FS_AIO is not selected this is an empty stub.
+ *
+ * Input Parameters:
+ *   pid - ID of the exiting task
+ *
+ * Returned Value:
+ *   None
+ *
+ ****************************************************************************/
+
+#ifdef CONFIG_FS_AIO
+void aio_cleanup(pid_t pid);
+#else
+#  define aio_cleanup(pid)
+#endif
+
 /* Work queue support is required.  The low-priority work queue is required
  * so that the asynchronous I/O does not interfere with high priority driver
  * operations.  If this pre-requisite is met, then asynchronous I/O support
diff --git a/sched/task/task_exithook.c b/sched/task/task_exithook.c
index 2944dc16ab6..d620b992e78 100644
--- a/sched/task/task_exithook.c
+++ b/sched/task/task_exithook.c
@@ -26,6 +26,7 @@
 
 #include <nuttx/config.h>
 
+#include <aio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <signal.h>
@@ -469,6 +470,12 @@ void nxtask_exithook(FAR struct tcb_s *tcb, int status)
   nxsig_discard_threadsignals(tcb);
 #endif
 
+  /* Cancel pending AIO before the task's stack is freed, so the worker
+   * thread does not dereference a stale aiocb after this task is gone.
+   */
+
+  aio_cleanup(tcb->pid);
+
   /* Leave the task group.  Perhaps discarding any un-reaped child
    * status (no zombies here!)
    */

Reply via email to