Skip to content

sched/group: Do not free the stacks of an ending process into its heap. - #21465

Draft
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:vfork-exit-stack-up
Draft

casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:vfork-exit-stack-up

Conversation

@casaroli

@casaroli casaroli commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

depends-on: /pull/21154

This branch holds the commit of #21154 and one commit of its own, because that commit changes the code that #21154 adds. Please review only the last commit.

Summary

In the kernel build the stack of a thread of an unprivileged group is a block of the user heap of the group. When a thread ends, nxsched_release_tcb() frees it with up_release_stack(), so the kernel works on the heap structures of the program. When the whole process ends, that heap goes away with the address environment anyway, and the program can have overwritten it: a stack overflow runs down through the heap, over the heap structure at its start.

When the group of a user thread ends (the last member leaves, or the group is exiting), group_leave() now clears TCB_FLAG_FREE_STACK, so that up_release_stack() does not free the stack into that heap. This also stops a free into the wrong heap when code in another process ends the thread. A vfork() child (GROUP_FLAG_VFORK, from #21154) keeps the flag: its stack is in the heap of its parent and is freed there. The stack of a thread that ends alone (pthread_exit()) is freed as before.

Impact

  • Kernel builds: stacks of an ending process are not freed into its own heap.
  • A task_restart() of a multi-threaded user process: the stacks of the killed threads are not freed any more. Before, they were freed from the context of the caller, into the heap of the caller. Read in the code, not run.
  • Flat and protected builds: no change. The code is under CONFIG_BUILD_KERNEL. No architecture code: every up_release_stack() frees only with this flag and clears the stack fields in any case.
  • No ABI change. Not a breaking change.

Testing

No test fails on master for this commit alone: a stack overflow test (a child that recurses, one that writes below a large alloca(), a thread that recurses) ends each child with SIGSEGV and the system goes on, with the same output on master and with this PR. With this commit no stack is freed into a heap that goes away, and the vfork and vexec cases below show that the stacks of vfork() children are still freed.

qemu-armv8a:knsh (QEMU, HVF) with #21154 applied, master 6b9b216, forkstress.c (see #21154):

Case #21154 and this PR
fork 1000, exec 300, pipe 200, nest 200, spawn 300 PASS, +0
vfork 200 PASS, +0
vexec 200 PASS, +0

ostest exits with status 0 on qemu-armv8a:knsh.

Builds with EXTRAFLAGS="-Wno-cpp -Werror", each OK and with group_leave.c compiled: qemu-armv8a:knsh, qemu-armv8a:nsh, rv-virt:pnsh64, mps2-an500:knsh, sim:nsh.

tools/checkpatch.sh -c -u -m -g passes.

…eap.

In the kernel build a vfork() child is in the address environment of its
parent.  group_allocate() and env_dup() put the task information and the
environment of the child in the user heap of the parent, because that is
the heap of the current address environment.  When the child ends,
task_uninit_info() returns at once for an unprivileged group, because
the user heap of the group goes away with its address environment.  The
heap of a vfork() child is the heap of its parent, and it stays.  Thus
each vfork() lost these blocks in the heap of the parent.  A shell runs
each command with vfork(), so its heap grew with each command.

Add GROUP_FLAG_VFORK.  nxtask_setup_fork() sets it when a vfork() child
joins the address environment of its parent.  task_uninit_info() frees
the task information and the environment of a group with this flag.
group_leave() clears the flag when the address environment of the caller
is not the one of the child (another process ends the child): a free
from there goes into the heap of the caller.  Then the blocks stay in the
heap of the parent, as before.

Flat and protected builds do not change: only CONFIG_ARCH_ADDRENV sets
the flag, and only CONFIG_BUILD_KERNEL uses it.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
In the kernel build the stack of a thread of an unprivileged group is a
block of the user heap of the group.  When a thread ends,
nxsched_release_tcb() frees it with up_release_stack(), which calls
kumm_free(): the kernel then works on the heap structures of the
program.  When the whole process ends, that heap goes away with the
address environment anyway, and the program can have overwritten it: a
stack overflow runs down through the heap, over the heap structure at
its start.  Seen with a thread that overflows its stack: the fatal
signal ends the process, and nxsig_terminate() -> nx_pthread_exit() ->
nxtask_exit() -> nxsched_release_tcb() -> up_release_stack() ->
mm_free() fails DEBUGASSERT(mm_heapmember()) on the broken heap.

When a user thread leaves its group, and the group ends (it is the
last member, or group_kill_children() set GROUP_FLAG_EXITING), clear
TCB_FLAG_FREE_STACK: every architecture frees the stack only when that
flag is set, and still clears the stack fields of the TCB.  This is
done in group_leave(), where the group is known: when the TCB is
released, it can have left the group already.  The stack of a thread
that ends alone (pthread_exit()) is freed as before.

A vfork() child (GROUP_FLAG_VFORK) keeps the flag: its stack is in the
heap of its parent, which stays.  Without this, each vfork() lost a
block of the size of the stack of the parent in the heap of the parent.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@casaroli casaroli added the Area: OS Components OS Components issues label Nov 4, 2026
@github-actions github-actions Bot added the Size: S The size of the change in this PR is small label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

arduino-mega2560

  • flash: .data +16 B (+0.0%, 66,241 B / 262,144 B, total: 25% used)
  • sram: .data +16 B (+0.6%, 2,811 B / 8,192 B, total: 34% used)

esp32-devkitc

  • ROM: .dram0.data +4 B, .flash.rodata +24 B (+0.0%, 129,108 B / 4,194,272 B, total: 3% used)
  • dram0_0_seg: .dram0.data +4 B (+0.0%, 10,028 B / 180,736 B, total: 6% used)
  • drom0_0_seg: .flash.rodata +24 B (+0.2%, 13,948 B / 4,194,272 B, total: 0% used)

hifive1-revb

  • flash: .data +8 B, .text +12 B (+0.0%, 85,744 B / 4,194,304 B, total: 2% used)
  • sram: .data +8 B (+0.2%, 3,964 B / 16,384 B, total: 24% used)

mirtoo

  • kseg0_progmem: .data +4 B, .text +12 B (+0.0%, 68,396 B / 131,072 B, total: 52% used)
  • kseg1_datamem: .data +4 B (+0.2%, 2,604 B / 32,256 B, total: 8% used)

qemu-armv8a

  • Code: .rodata +16 B (+0.0%, 360,196 B)
  • Data: .data.g_version +2 B (+0.0%, 77,919 B)

rx65n-rsk2mb

  • RAM: .data +4 B (+0.1%, 6,702 B / 262,144 B, total: 3% used)
  • ROM: .rodata +16 B (+0.0%, 91,420 B / 2,097,152 B, total: 4% used)

stm32-nucleo-f103rb

  • flash: .text +16 B (+0.0%, 34,812 B / 131,072 B, total: 27% used)

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: OS Components OS Components issues Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant