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]
