ricardgb opened a new issue, #19308:
URL: https://github.com/apache/nuttx/issues/19308

   > **Disclaimer:** This report was prepared with AI assistance (Claude Code). 
I reviewed it, verified the code citations against master myself, and validated 
the fix on real hardware (deterministic `CONFIG_MM_FILL_ALLOCATIONS` A/B) 
before filing.
   
   ## Summary
   On a **non-SMP** build, `nxtask_exit()` (`sched/task/task_exit.c`) 
identifies the
   exiting task with `dtcb = this_task()` — the head of the ready-to-run list. 
If a
   higher-priority task is scheduled to the ready-to-run head while the context
   switch 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 / 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 uses the ready-to-run head.
   
   ## Affected code
   `sched/task/task_exit.c`, `nxtask_exit()`
   (https://github.com/apache/nuttx/blob/master/sched/task/task_exit.c#L75-L88)
   
   ```c
   #ifdef CONFIG_SMP
     dtcb = current_task(this_cpu());   /* running task  (correct) */
   #else
     dtcb = this_task();                /* ready-to-run head (WRONG when head 
!= running) */
   #endif
     ...
     nxsched_remove_self(dtcb);            /* removes the wrong task            
*/
     ret = nxsched_release_tcb(dtcb, ...); /* frees the wrong task's TCB/stack  
*/
   ```
   
   Note: `this_task()` and `current_task(cpu)` are the *same* expression on 
non-SMP
   (both `g_readytorun.head`), so simply dropping the `#ifdef` is a no-op. The
   correct non-SMP accessor for the running task is 
`g_running_tasks[this_cpu()]`.
   
   ## Root cause
   `this_task()` returns `g_readytorun.head`. NuttX already distinguishes the
   ready-to-run head from the actually-running task — see the `running_task()`
   macro and `g_running_tasks[]` (whose comment notes they "may differ during
   interrupt-level context switches"). During a task's exit the two can diverge 
and
   stay diverged into `nxtask_exit()`:
   
   1. The task calls `exit()` → `_exit()` enters a critical section and runs
      `nxtask_exithook()` (for a `CONFIG_BINFMT_LOADABLE` app this performs the 
full
      module teardown: `binfmt_exit()` → `unload_module()` → 
`elf_unloadbinary()` →
      `libelf_uninit()` → `up_textheap_free()`), then `up_exit()` → 
`nxtask_exit()`.
   2. During that long, preemptible window a higher-priority task becomes 
ready, is
      placed at the ready-to-run head and marked `TSTATE_TASK_RUNNING`, with the
      register-level switch deferred. The exiting task is demoted to
      `TSTATE_TASK_READYTORUN` but keeps executing; `g_running_tasks[cpu]` still
      points at it.
   3. `nxtask_exit()` reads `dtcb = this_task()` = the higher-priority task and
      removes/frees **it**.
   
   `up_exit()`'s own comment — "Destroy the task at the head of the ready to run
   list" — encodes the assumption that the head *is* the exiting task, which 
step 2
   violates.
   
   ## Steps to reproduce
   1. Non-SMP target with the ELF loader (`CONFIG_ELF`, 
`CONFIG_BINFMT_LOADABLE`,
      `CONFIG_LIBC_ELF`, `CONFIG_NSH_FILE_APPS`) plus at least one high-priority
      kernel thread that can wake during the test (e.g. a Wi-Fi/driver worker).
   2. Build a minimal relocatable ELF app — an empty `int main(void){return 
0;}` is
      sufficient; the crash is independent of app content.
   3. `nsh> /path/to/app.elf`
   4. The board hard-faults / locks up on task exit. Because it is a 
use-after-free
      the crash is timing-dependent; enabling `CONFIG_MM_FILL_ALLOCATIONS` 
(which
      poisons freed memory) makes it deterministic — the freed higher-priority 
TCB
      is later restored with a corrupted/zeroed PC.
   
   Expected: the app runs and exits; nsh returns to the prompt.
   Actual: hard fault / lockup (a still-runnable task's TCB was freed).
   
   ## Evidence (captured on target with a minimal, no-syslog SRAM breadcrumb)
   Recorded at the top of `nxtask_exit()` (raw word stores only, to avoid
   perturbing the race): `this_task()`, `g_running_tasks[cpu]`, the chosen 
`dtcb`,
   and their `task_state`s.
   
   - Crashing app exit (app = pid 8, prio 100):
     `dtcb = pid4 / prio224 / RUNNING` (a live high-prio Wi-Fi thread),
     `g_running_tasks[0] = pid8 / prio100 / READYTORUN` (the exiting app) →
     **dtcb != running: the wrong TCB is freed.**
   - Fault registers on the deterministic (`MM_FILL`) crash: `CFSR=0x00020000`
     (INVSTATE), `HFSR=0x40000000` (FORCED), stacked **PC=0x00000000**.
   - Normal exits show `dtcb == running == head`, all `RUNNING`.
   
   ## Suggested fix
   Use the actually-running task in the non-SMP path (as SMP already does):
   
   ```diff
    #ifdef CONFIG_SMP
      dtcb = current_task(this_cpu());
    #else
   -  dtcb = this_task();
   +  dtcb = g_running_tasks[this_cpu()];
    #endif
   ```
   
   `g_running_tasks[this_cpu()]` is updated only at real context switches, so it
   stays correct when the ready-to-run head has been taken over by a deferred
   higher-priority switch. 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.
   
   ## Validation (on the affected target)
   With `CONFIG_MM_FILL_ALLOCATIONS=y` so the use-after-free faults 
deterministically
   and the only difference between builds is the one line:
   1. Crash A/B: the pre-fix build hard-faults on the first ELF run; the fixed 
build
      survives 20+ ELF load/run/exit cycles with the board healthy and no heap 
growth.
   2. Breadcrumb: with the fix, in the exact divergence condition (head = the
      high-prio thread) `dtcb` is now the exiting app (`dtcb == running`) and 
the
      wrong-TCB condition never occurs.
   
   ## Related
   Same failure family as #17418 / #17564 ("group released ahead of schedule"),
   which address a different premature-release ordering; this issue is 
specifically
   `nxtask_exit()` choosing the wrong `dtcb` via `this_task()` on non-SMP.
   
   ## Environment
   - **NuttX version:** 12.13.0 (also present on master, checked 2026-07-02)
   - **Architecture:** arm — Cortex-M33, RP2350, non-SMP (`CONFIG_SMP` not set, 
`CONFIG_BUILD_FLAT=y`)
   - **Area:** Kernel / scheduler (`sched/task/task_exit.c`)
   - **Relevant config:** `CONFIG_ELF=y`, `CONFIG_BINFMT_LOADABLE=y`, 
`CONFIG_LIBC_ELF=y`, `CONFIG_NSH_FILE_APPS=y`
   - **Toolchain / host:** arm-none-eabi-gcc 13.2.1; Ubuntu 24.04.4 LTS (Linux 
6.17.0-35-generic)
   - Reproduced on a local raspberrypi-pico-2-w port, but the defect is in 
generic scheduler code and applies to any non-SMP configuration with a 
long/preemptible exit path (e.g. `CONFIG_BINFMT_LOADABLE`) and a 
higher-priority ready task.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to