From 5931417f48ac4e90317772e8671a568de8010c51 Mon Sep 17 00:00:00 2001 From: yi chen <94xhn1@gmail.com> Date: Sat, 11 Jul 2026 04:54:07 +0800 Subject: [PATCH] sched/task: fix nxtask_exit() freeing the wrong TCB on non-SMP (#19308) On a non-SMP build, nxtask_exit() identifies the exiting task with dtcb = this_task() - the head of the ready-to-run list. If a higher-priority task becomes ready and is placed at the ready-to-run head while the context switch away from the exiting task is still deferred (so the head no longer equals the task that is actually running), nxtask_exit() calls nxsched_remove_self(dtcb) and nxsched_release_tcb(dtcb) on that higher-priority task instead of on the task that is actually exiting. This frees the TCB/stack of a live task - a use-after-free that hard-faults or locks up the board. The SMP path of the same function already avoids this by using the actually-running task (current_task(this_cpu()) == g_assignedtasks[cpu]); only the non-SMP path used the ready-to-run head. Note that this_task() and current_task(cpu) are literally the same expression on non-SMP (both resolve to the ready-to-run list head), so the divergence isn't about SMP vs non-SMP semantics per se - it's that the non-SMP path never had a correct "actually running" accessor to fall back to. The correct non-SMP accessor for the running task is g_running_tasks[this_cpu()] (this_cpu() is (0) on non-SMP), which is updated only at real context switches and so stays correct exactly when the ready-to-run head has diverged due to a deferred higher-priority switch. Note this is deliberately NOT the same as the existing running_task() macro (running_task() == up_interrupt_context() ? g_running_tasks[this_cpu()] : this_task()), since that macro only prefers g_running_tasks[] during interrupt-level context switches - the race this issue describes happens in a perfectly ordinary (non-interrupt) call chain (exit() -> _exit() -> nxtask_exithook() -> up_exit() -> nxtask_exit()), so running_task() would still resolve to this_task() here and would not fix anything. Removing the correct task is safe: the exiting task is in TSTATE_TASK_READYTORUN and present in the ready-to-run list, so nxsched_remove_readytorun() takes its dq_rem path, and the CPU then switches to the pending higher-priority task. nxtask_exit() is the only exit-path site that derives the exiting task from this_task() (nxtask_terminate() uses an explicit pid), so this one line is the complete fix. Fixes #19308 ## Verification I don't have the affected hardware (RP2350/non-SMP) set up to reproduce the exact race, but the issue report itself includes extensive on-target validation: the reporter captured the divergence with an SRAM breadcrumb (this_task() resolving to a live high-priority Wi-Fi thread while g_running_tasks[cpu] correctly pointed at the exiting app), reproduced the resulting hard fault deterministically with CONFIG_MM_FILL_ALLOCATIONS, and validated this exact one-line fix survives 20+ ELF load/run/exit cycles where the pre-fix build hard-faults on the first run. I independently re-verified the code citations against the current upstream master (confirmed the non-SMP path still uses this_task(), confirmed g_running_tasks[]/this_cpu() are unconditionally available per include/nuttx/sched.h, and confirmed running_task() would not have fixed this given the non-interrupt call path), and verified the core divergence logic (this_task() resolving to the wrong task vs. g_running_tasks[this_cpu()] resolving to the actually-exiting task) with a standalone C reproduction of the two accessors under the described race condition. Generative AI (Claude) was used to help investigate this issue and implement this fix. All changes were reviewed by me before submission. Signed-off-by: yi chen <94xhn1@gmail.com> --- sched/task/task_exit.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sched/task/task_exit.c b/sched/task/task_exit.c index e79c4f812504f..474f9bbdd5d72 100644 --- a/sched/task/task_exit.c +++ b/sched/task/task_exit.c @@ -77,14 +77,18 @@ int nxtask_exit(void) FAR struct tcb_s *dtcb; FAR struct tcb_s *rtcb; int ret; -#ifdef CONFIG_SMP /* Avoid using this_task() because it may assume a state that is not - * appropriate for an exiting task. + * appropriate for an exiting task: if a higher-priority task became + * ready and was placed at the head of the ready-to-run list while + * the context switch away from the exiting task is still deferred, + * this_task() (the ready-to-run head) no longer identifies the task + * that is actually exiting. */ +#ifdef CONFIG_SMP dtcb = current_task(this_cpu()); #else - dtcb = this_task(); + dtcb = g_running_tasks[this_cpu()]; #endif sinfo("%s pid=%d,TCB=%p\n", get_task_name(dtcb),