Skip to content

0.13.3 --- a detached thread ends and the program goes on - #32

Merged
Sunrisepeak merged 4 commits into
mainfrom
detached-exit-own-stack
Sep 13, 2026
Merged

0.13.3 --- a detached thread ends and the program goes on#32
Sunrisepeak merged 4 commits into
mainfrom
detached-exit-own-stack

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 13, 2026

Copy link
Copy Markdown
Member

Every program whose detached thread ended stopped with an access violation on arm64 macOS, and on every target the end of a detached thread wrote over memory it did not own.

musl releases a detached thread's mapping from inside the thread. On Linux the thread stands on that mapping, so src/thread/__unmapself.c first moves to a 256-byte stack shared by every exiting thread and makes the two system calls that end the thread from there. Neither premise holds in this port:

  • __clone never runs a context on the stack musl allocated; kal_task_start supplies its own. The move protects nothing.
  • The two calls are this port's. Each passes through the dispatcher, the context table and openkal, and in an unoptimized build that path needs far more than 256 bytes.

Measured with gdb on x86_64 Linux, dev profile, stopped in kal_task_current under SYS_exit on the shared stack:

shared_stack=[0x4b3300, 0x4b3400)
in kal_task_current: sp=0x4afdb8, below shared_stack by 13640 bytes

What the overflow lands on depends on the link. On arm64 macOS the bss beneath shared_stack is musl's thread-specific key table and then this port's context table (g_slots), so the exiting thread read its own record out of the bytes it had just written and longjmped through them. lldb on macos-14, lsp-mcpp's server after its stdin reader thread ended:

* thread #2, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: __pthread_exit(result=0x0) at pthread_create.c:169:11   a_store(&self->detach_state, DT_EXITED)
    sp = 0x0000000100343da0  lsp-mcpp`buf + 16                         (beside _unmap_base, not the thread's stack)

On Linux the same bytes land on data that happened not to matter, which is why only macOS showed it.

  • port/src/okm_thread.c: __unmapself releases the mapping from the stack the thread is on and ends the thread the ordinary way. What musl's version guaranteed, that nothing runs upon the mapping once released, holds without the move because nothing here ever ran upon it.
  • mcpp.toml: musl/src/thread/__unmapself.c is excluded, with the reason beside it.
  • musl/PATCHES.md, README.md: twelve replaced sources.
  • examples/threads-detached: eight detached threads end one after another, then a key with a destructor and a joinable thread's error value exercise the two tables beneath the old shared stack. CI runs it on every row.
  • 0.13.3.

Measured locally: the probe passes on x86_64 Linux and, cross-built, on x86_64-windows-gnu under Wine, and the shared_stack symbol is gone from the image. With openkal-llvm-runtime pointed at this tree, all 15 of lsp-mcpp's unit-test programs pass for x86_64-windows-gnu under Wine. The macOS row of this CI is the one that failed before.

Found through lsp-mcpp, whose server crashed on macos-14 as soon as its input reader thread ended.

Also carries openkal-windows 0.7.3 (mcpplibs/openkal-windows#21 and #22; mcpp-index#417 and #418). Its requirement here is exact, so this release is how consumers of openkal-musl reach them:

  • 0.7.2: a channel's ends reach only the program they are placed in. A child whose input was a channel never saw the end of it.
  • 0.7.3: a started program receives its arguments and working directory as given. Backslashes were dropped from the command line, every element was quoted so the command interpreter could not read its switches, and the directory carried the \\?\ prefix that the interpreter refuses.

musl's __unmapself moves to a 256-byte stack shared by every exiting thread
before the two calls that end a detached one. Here the thread runs on the stack
kal_task_start supplied, and those calls pass through the port's dispatcher,
context table and openkal: an unoptimized build reached 13,640 bytes below the
shared stack. On arm64 macOS the overflow overwrote the thread-specific key
table and the context table, and the exit jumped through the bytes it had just
written. The port releases the mapping from the stack the thread is on;
examples/threads-detached runs on every CI row.
…m they are placed in

The version table in README.md names the runtime that carries each release since
0.11.0.
@Sunrisepeak
Sunrisepeak merged commit 2262788 into main Sep 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant