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!) */
