Skip to content

Commit 2262788

Browse files
authored
0.13.3 --- a detached thread ends and the program goes on (#32)
musl's __unmapself moved 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. Carries openkal-windows 0.7.3: a channel's ends reach only the program they are placed in (0.7.2), and a started program receives its arguments and directory as given (0.7.3).
1 parent dc84bf5 commit 2262788

9 files changed

Lines changed: 185 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,19 @@ jobs:
606606
bash tools/run-probe.sh examples/threads-cxx threads-cxx
607607
grep -q 'value 42, result is the argument' examples/threads-cxx/run.log
608608
609+
# ⭐ A DETACHED THREAD ENDS, AND THE PROGRAM THAT STARTED IT GOES ON.
610+
#
611+
# musl's `__unmapself` left for a 256-byte shared stack before the calls
612+
# that end a detached thread; this port's path for those calls needs far
613+
# more, and on macOS the overflow overwrote the context table the exit then
614+
# read. musl/PATCHES.md, `src/thread/__unmapself.c`.
615+
- name: A detached thread ends and the program goes on
616+
env:
617+
MCPP_TARGET: ${{ matrix.target }}
618+
run: |
619+
bash tools/run-probe.sh examples/threads-detached threads-detached
620+
grep -q 'detached: 8 started, 8 ended' examples/threads-detached/run.log
621+
609622
- name: A program may use the names the internal overlay defines
610623
env:
611624
MCPP_TARGET: ${{ matrix.target }}

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ consumer needs to answer it without asking.
4141

4242
| this package | is carried by |
4343
| --- | --- |
44+
| 0.13.3 | `openkal-llvm-runtime = "0.9.4"` |
45+
| 0.13.2 | `openkal-llvm-runtime = "0.9.3"` |
46+
| 0.13.1 | `openkal-llvm-runtime = "0.9.2"` |
47+
| 0.13.0 | `openkal-llvm-runtime = "0.9.0"` |
48+
| 0.12.0 | `openkal-llvm-runtime = "0.8.0"` |
4449
| 0.11.0 | `openkal-llvm-runtime = "0.7.0"` |
4550
| 0.10.0 | `openkal-llvm-runtime = "0.6.0"` |
4651
| 0.9.0 | `openkal-llvm-runtime = "0.5.0"` |
@@ -76,8 +81,8 @@ architecture. Replacing that one header is the whole of the redirection; the
7681

7782
`musl/PATCHES.md` lists the whole of what is not unmodified: **five patched
7883
lines**, all of one kind — a machine word carried through a variable declared
79-
`long`, which is not a machine word on one of the three targets — and **eleven
80-
replaced sources**. Five of the eleven are replaced for the same reason: each
84+
`long`, which is not a machine word on one of the three targets — and **twelve
85+
replaced sources**. Five of the twelve are replaced for the same reason: each
8186
reads the shape of one particular environment rather than asking a kernel for
8287
something.
8388

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "threads-detached"
3+
version = "0.1.0"
4+
5+
[dependencies]
6+
openkal-musl = { path = "../.." }
7+
8+
[targets.threads-detached]
9+
kind = "bin"
10+
main = "src/main.c"
11+
12+
[build]
13+
cxx_runtime = "host-coupled"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* A detached thread ends, and the program that started it goes on.
2+
*
3+
* musl releases a detached thread's mapping from inside the thread, and on Linux
4+
* the thread is standing on that mapping --- so `__unmapself' first moves to a
5+
* 256-byte stack every exiting thread shares, and makes the two system calls
6+
* that end the thread from there. In this port neither call is a system call:
7+
* each passes through the port's dispatcher, the context table and openkal,
8+
* and an unoptimized build of that path needs several times 256 bytes. The
9+
* overflow wrote over whatever the linker placed beneath the shared stack. On
10+
* macOS that is musl's table of thread-specific keys and this port's context
11+
* table, and the thread then looked up its own record in the table it had just
12+
* overwritten and jumped into it: every program whose detached thread ended
13+
* stopped with an access violation.
14+
*
15+
* ⭐ WHAT IS OBSERVED. Detached threads that end one after another, then the
16+
* two tables beneath the shared stack in use: a key with a destructor, and a
17+
* joinable thread's own error value, which is reached through the context table.
18+
*/
19+
#include <errno.h>
20+
#include <pthread.h>
21+
#include <stdio.h>
22+
#include <time.h>
23+
24+
enum { DETACHED = 8 };
25+
26+
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
27+
static pthread_cond_t reported = PTHREAD_COND_INITIALIZER;
28+
static int started;
29+
static int ended;
30+
31+
static void* detached(void* arg)
32+
{
33+
(void)arg;
34+
pthread_mutex_lock(&lock);
35+
++ended;
36+
pthread_cond_broadcast(&reported);
37+
pthread_mutex_unlock(&lock);
38+
return 0;
39+
}
40+
41+
static int destroyed;
42+
static void destroy(void* value) { if (value == &destroyed) ++destroyed; }
43+
44+
static pthread_key_t key;
45+
static void* keyed(void* arg)
46+
{
47+
(void)arg;
48+
errno = 0;
49+
pthread_setspecific(key, &destroyed);
50+
errno = 7;
51+
return (void*)(long)errno;
52+
}
53+
54+
static void pause_briefly(void)
55+
{
56+
struct timespec ts = { 0, 50 * 1000 * 1000 };
57+
nanosleep(&ts, 0);
58+
}
59+
60+
int main(void)
61+
{
62+
int failures = 0;
63+
64+
pthread_attr_t attr;
65+
pthread_attr_init(&attr);
66+
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
67+
for (int i = 0; i < DETACHED; ++i) {
68+
pthread_t thread;
69+
if (pthread_create(&thread, &attr, detached, 0) != 0) {
70+
printf("FAIL: detached thread %d was not created\n", i);
71+
++failures;
72+
break;
73+
}
74+
++started;
75+
pthread_mutex_lock(&lock);
76+
while (ended < started) pthread_cond_wait(&reported, &lock);
77+
pthread_mutex_unlock(&lock);
78+
/* Reporting precedes ending; the pause lets the thread finish ending
79+
* before the next one starts, which is where the fault was. */
80+
pause_briefly();
81+
}
82+
pthread_attr_destroy(&attr);
83+
printf("detached: %d started, %d ended\n", started, ended);
84+
if (ended != DETACHED) { printf("FAIL: %d of %d detached threads ended\n", ended, DETACHED); ++failures; }
85+
86+
if (pthread_key_create(&key, destroy) != 0) { puts("FAIL: pthread_key_create"); ++failures; }
87+
pthread_t joinable;
88+
void* result = 0;
89+
if (pthread_create(&joinable, 0, keyed, 0) != 0 || pthread_join(joinable, &result) != 0) {
90+
puts("FAIL: the joinable thread did not run");
91+
++failures;
92+
}
93+
printf("joinable: its error value %ld, destructor ran %d time(s)\n", (long)result, destroyed);
94+
if ((long)result != 7) { puts("FAIL: the joinable thread's error value"); ++failures; }
95+
if (destroyed != 1) { puts("FAIL: the key's destructor"); ++failures; }
96+
97+
printf("-- failures: %d --\n", failures);
98+
return failures == 0 ? 0 : 1;
99+
}

mcpp.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-musl"
4-
version = "0.13.2"
4+
version = "0.13.3"
55
description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel."
66
license = "Apache-2.0"
77

@@ -69,7 +69,7 @@ openkal-opensbi = { version = "0.6.0", features = ["standalone"] }
6969
defines = ["OKM_HAS_FS=0", "OKM_HAS_PROCESS=0", "OKM_HAS_TASK=0"]
7070

7171
[target.'cfg(windows)'.dependencies]
72-
openkal-windows = { version = "0.7.0", features = ["standalone"] }
72+
openkal-windows = { version = "0.7.3", features = ["standalone"] }
7373

7474
# The feature macros musl's own build establishes.
7575
#
@@ -115,6 +115,10 @@ sources = [
115115
"!musl/src/env/__init_tls.c",
116116
"!musl/src/thread/__set_thread_area.c",
117117
"!musl/src/thread/clone.c",
118+
# Replaced in port/src/okm_thread.c: musl leaves for a 256-byte shared stack
119+
# before the calls that end a detached thread, and this port's path for those
120+
# calls needs far more --- it overwrote the context table on macOS.
121+
"!musl/src/thread/__unmapself.c",
118122
"!musl/src/process/posix_spawn.c",
119123
# ⭐ AND ITS SIBLING, WHICH IS EXCLUDED BECAUSE THE ONE ABOVE IS.
120124
# musl's posix_spawnp does not search a PATH: it stores `__execvpe' in the

musl/PATCHES.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ carry it in a `long`.
9393

9494
## The sources this port replaces, and why each
9595

96-
Eleven, and the list in the manifest carries the same reasons. Five read the shape
96+
Twelve, and the list in the manifest carries the same reasons. Five read the shape
9797
of one environment directly. Two carry a machine word through a variable
98-
declared `long`. Two more were found only by running the result. And one is
98+
declared `long`. Three more were found only by running the result. And one is
9999
replaced because another already was:
100100

101101
`src/process/posix_spawnp.c` does not search a PATH. It stores `__execvpe` in
@@ -117,6 +117,18 @@ one in musl's own source, and having the two ways of searching for one program
117117
disagree with each other is worse for a caller than having both wrong the same
118118
way. Nothing on that target searches a PATH today: its CI row declares no shell.
119119

120+
⚠️⚠️ `src/thread/__unmapself.c` moves to a 256-byte stack shared by every exiting
121+
thread before it makes the two calls that end a detached one. On Linux the thread
122+
stands on the mapping it is about to release, and two raw system calls fit in
123+
256 bytes. Here the thread stands on the stack `kal_task_start` supplied, and
124+
the calls pass through this port's dispatcher, its context table and openkal:
125+
an unoptimized build reached **13,640 bytes** below the shared stack, measured on
126+
x86_64 Linux. The overflow lands on whatever the linker placed beneath it. On
127+
arm64 macOS that is the table of thread-specific keys and then the context
128+
table, so the exiting thread read its own record out of the bytes it had just
129+
written and jumped through them. `port/src/okm_thread.c` releases the mapping
130+
from the stack the thread is on; `examples/threads-detached` is the probe.
131+
120132
`src/mman/mmap.c` returns a pointer through a `long`. It is replaced rather than
121133
patched because the replacement is also better where a `long` does hold a
122134
pointer: the value never becomes an integer at all.

port/src/okm_thread.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <string.h>
3131
#include <time.h>
3232
#include "futex.h"
33+
#include "syscall.h"
3334

3435
void __okm_set_tp(uintptr_t value);
3536
void* __okm_get_self(void);
@@ -153,6 +154,36 @@ syscall_arg_t __okm_task_exit(int code)
153154
return 0;
154155
}
155156

157+
/* ⚠️⚠️ A DETACHED CONTEXT RELEASES ITS MAPPING FROM WHERE IT STANDS, AND THE
158+
* STACK MUSL MOVED TO FIRST WAS OVERRUN FIFTY TIMES OVER.
159+
*
160+
* A detached thread releases its own mapping as the last thing it does. On Linux
161+
* the thread is standing on that mapping, so musl's `__unmapself' first moves to
162+
* a 256-byte stack shared by every exiting thread and makes the two system calls
163+
* that end the thread from there --- two instructions each, which is what the
164+
* size was chosen for.
165+
*
166+
* Neither premise holds here. `__clone' above never runs a context on the stack
167+
* musl allocated, because kal_task_start supplies its own, so the move protects
168+
* nothing. And the two calls are this port's: each passes through the
169+
* dispatcher, the context table and openkal. Measured in an unoptimized build for
170+
* x86_64 Linux, that path reached 13,640 bytes below the shared stack, writing
171+
* over whatever the linker had placed beneath it. What that is depends on the
172+
* target. On arm64 macOS it is musl's table of thread-specific keys and then this
173+
* port's context table, so the thread looked up its own record in the table it
174+
* had just overwritten and jumped into it: every program whose detached thread
175+
* ended stopped there with an access violation (examples/threads-detached).
176+
*
177+
* ⇒ The mapping is released from the stack the context is on, and the end is
178+
* reached the ordinary way. What musl's version guaranteed --- that nothing runs
179+
* upon the mapping once it is released --- holds without the move, because
180+
* nothing here ever ran upon it. */
181+
void __unmapself(void* base, size_t size)
182+
{
183+
__syscall(SYS_munmap, base, size);
184+
__syscall(SYS_exit, 0);
185+
}
186+
156187
/* --- the suspension primitive ------------------------------------------------ */
157188

158189
syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespec* t)

tools/cross-build-macos.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ cd "$here"
6565

6666
# Kept in step with mcpp.toml, INCLUDING that system's own exclusions:
6767
# okm_phdr.c answers dl_iterate_phdr from an ELF header and that format has none.
68-
skip='__libc_start_main|__init_tls|__set_thread_area|clone|posix_spawn|posix_spawnp|mmap|syscall_ret|getcwd|fcntl|dl_iterate_phdr|okm_phdr|cache'
68+
skip='__libc_start_main|__init_tls|__set_thread_area|__unmapself|clone|posix_spawn|posix_spawnp|mmap|syscall_ret|getcwd|fcntl|dl_iterate_phdr|okm_phdr|cache'
6969
for f in musl/src/*/*.c musl/src/malloc/mallocng/*.c port/src/*.c port/src/*.S; do
7070
base=$(basename "$f"); base=${base%.*}
7171
[[ "$base" =~ ^($skip)$ ]] && continue

tools/probe-cross-macos.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ cd "$here"
8686
# SECOND statement of what mcpp.toml already states, and a second statement is
8787
# a thing that falls behind the first. It fell behind on the release that added
8888
# the tenth entry, and it is this job that said so.
89-
skip='__libc_start_main|__init_tls|__set_thread_area|clone|posix_spawn|posix_spawnp|mmap|syscall_ret|getcwd|fcntl|dl_iterate_phdr|okm_phdr|cache'
89+
skip='__libc_start_main|__init_tls|__set_thread_area|__unmapself|clone|posix_spawn|posix_spawnp|mmap|syscall_ret|getcwd|fcntl|dl_iterate_phdr|okm_phdr|cache'
9090
units=0
9191
for f in musl/src/*/*.c musl/src/malloc/mallocng/*.c port/src/*.c port/src/*.S; do
9292
base=$(basename "$f"); base=${base%.*}

0 commit comments

Comments
 (0)