From 4a04f457c1d666c7f3b05a9ff4b6946d59f80abe Mon Sep 17 00:00:00 2001 From: Cloud_Yun Date: Fri, 2 Oct 2026 12:37:31 +0900 Subject: [PATCH 1/2] pthread_getattr_np reports ENOSYS openkal reports no bounds for the stack a context runs on, so musl's pthread_getattr_np described a range that was not the stack: a page of the static auxiliary vector for the first context, and for a started one the mapping pthread_create allocated and the context never runs on. The source is excluded and a replacement in okm_thread.c reports ENOSYS, which is what getrlimit(RLIMIT_STACK) already answers. The name is added to [c-abi-absent] and to the README table, and examples/stack-bounds checks both cases. --- .github/workflows/ci.yml | 7 +++++++ README.md | 1 + examples/stack-bounds/mcpp.toml | 13 ++++++++++++ examples/stack-bounds/src/main.c | 36 ++++++++++++++++++++++++++++++++ mcpp.toml | 3 +++ port/src/okm_thread.c | 8 +++++++ 6 files changed, 68 insertions(+) create mode 100644 examples/stack-bounds/mcpp.toml create mode 100644 examples/stack-bounds/src/main.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f75ed0..e51f834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -780,6 +780,13 @@ jobs: bash tools/run-probe.sh examples/threads-detached threads-detached grep -q 'detached: 8 started, 8 ended' examples/threads-detached/run.log + - name: pthread_getattr_np reports that it cannot say + env: + MCPP_TARGET: ${{ matrix.target }} + run: | + bash tools/run-probe.sh examples/stack-bounds stack-bounds + grep -q 'stack bounds: first 38, started 38' examples/stack-bounds/run.log + # A LARGE ALLOCATION IS A MAPPING, AND A MAPPING IS WHOLE PAGES. # # musl's allocator uses a mapping up to the end of its last page; the port diff --git a/README.md b/README.md index 3c184f9..f44ec31 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,7 @@ lesser of the two, and the row returns when the schema carries `targets`. | --- | --- | --- | | signal handlers | `sigaction` reports `ENOSYS` for any handler other than the default or ignore. **Since 0.16.0 a disposition is accepted only where it is the one already in effect**: `SIG_DFL` succeeds for every signal but `SIGPIPE`, `SIG_IGN` succeeds for `SIGPIPE` alone, and the enquiry reports `SIG_IGN` for `SIGPIPE` rather than a zeroed record | openkal has no asynchronous delivery. A handler that was accepted and could never run would be silently wrong; masking, which has nothing to mask, succeeds. Until 0.16.0 `SIG_IGN` was accepted for every signal and installed for none, so a program that asked not to be ended by the interrupt keystroke was told it had succeeded and was ended by it. `SIGPIPE` is the one disposition that is not the default, and not by accident: openkal requires a write to a stream whose far end is gone to report the condition rather than end the program, so an implementation beneath has already arranged that the signal does nothing. | | a terminal's whole state | `tcgetattr` and `tcsetattr` carry line assembly, the echo, and whether the environment reserves keystrokes — the three positions openkal names. **Since 0.16.0 they reach the terminal**: `TCGETS`, `TCSETS`/`TCSETSW`/`TCSETSF` and `TIOCGWINSZ` are performed through `openkal.terminal`, so `cfmakeraw` followed by `tcsetattr` puts the terminal into raw mode and the interrupt keystroke arrives as the byte `0x03`. What a program cannot change is everything the structure carries that openkal does not name: output post-processing (`OPOST`), the line speed, the control characters, `VMIN`/`VTIME`, and the draining the `W` and `F` forms ask for. A `tcsetattr` that alters one of them is accepted and that part has no effect --- measurably: a program in raw mode that writes a newline still gets a carriage return before it, where the same program above the system's own C library does not; `tcgetattr` reports the composition port/src/okm_syscall.c states | openkal's mode word has three positions and `struct termios` has four flag words and twenty characters. The three are the ones a program needs in order to read keystrokes; the rest are either the terminal's own (the speed, the characters) or output-side, and openkal names none of them. Until 0.16.0 `TCGETS` and `TIOCGWINSZ` reported success and wrote nothing into the caller's structure while `TCSETS` was refused, which is mcpplibs/openkal-musl#36. A program that wants a read to give up asks `kal_timeout_read`, which is where openkal states a bound upon waiting. | +| a stack's bounds | `pthread_getattr_np` reports `ENOSYS` for every thread | openkal reports no bounds for the stack a context runs on. What musl would compute for the main thread starts from the auxiliary vector, which here is a static array, and a thread's stack is the one `kal_task_start` supplied, not the mapping musl allocated for it. | | memory protection | `mprotect` reports `ENOSYS` | openkal has no operation upon a mapping's protection. musl asks for a guard page below a thread's stack and proceeds without one when told this, so the honest answer is also the one it is prepared for. | | out-of-band data | `MSG_OOB`, `MSG_PEEK`, and `POLLPRI` are never reported and `recv` refuses the flags | openkal's transfer operations move bytes and have no second channel and no non-destructive read. | | readiness *sets* | `epoll` is not built at all, so the link names it | a set held by the environment is a facility of one kernel rather than a capability. `poll` and `select` ask each descriptor in turn, which is what an interface without a set permits. | diff --git a/examples/stack-bounds/mcpp.toml b/examples/stack-bounds/mcpp.toml new file mode 100644 index 0000000..8c94aca --- /dev/null +++ b/examples/stack-bounds/mcpp.toml @@ -0,0 +1,13 @@ +[package] +name = "stack-bounds" +version = "0.1.0" + +[dependencies] +openkal-musl = { path = "../.." } + +[targets.stack-bounds] +kind = "bin" +main = "src/main.c" + +[build] +cxx_runtime = "host-coupled" diff --git a/examples/stack-bounds/src/main.c b/examples/stack-bounds/src/main.c new file mode 100644 index 0000000..f263b23 --- /dev/null +++ b/examples/stack-bounds/src/main.c @@ -0,0 +1,36 @@ +/* pthread_getattr_np reports that it cannot say, for the first context and for a started one. + * + * openkal reports no bounds for the stack a context runs on. The range musl + * would compute is a page of the port's static auxiliary vector for the first + * context, and for a started one the mapping pthread_create allocated and the + * context never runs on; a caller that trusts either walks off the real stack. + */ +#define _GNU_SOURCE +#include +#include +#include + +static void* body(void* arg) +{ + pthread_attr_t a; + *(int*)arg = pthread_getattr_np(pthread_self(), &a); + return 0; +} + +int main(void) +{ + int failures = 0; + pthread_attr_t a; + int first = pthread_getattr_np(pthread_self(), &a); + int started = 0; + pthread_t t; + if (pthread_create(&t, 0, body, &started) != 0 || pthread_join(t, 0) != 0) { + puts("FAIL: the thread did not run"); + ++failures; + } + printf("stack bounds: first %d, started %d\n", first, started); + if (first != ENOSYS) { puts("FAIL: the first context's stack bounds"); ++failures; } + if (started != ENOSYS) { puts("FAIL: a started context's stack bounds"); ++failures; } + printf("-- failures: %d --\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/mcpp.toml b/mcpp.toml index 9e4b58e..9f74296 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -117,6 +117,7 @@ fchmodat = { form = "enosys", note = "as chmod" } chown = { form = "enosys", note = "a capability-oriented environment has no principal for an owner to name" } fchown = { form = "enosys", note = "as chown" } lchown = { form = "enosys", note = "as chown" } +pthread_getattr_np = { form = "enosys", note = "openkal reports no bounds for the stack a context runs on" } # The call succeeds and part of what it asked for is not done. Each of these # is a place where refusing would be worse than the partial answer, and the @@ -257,6 +258,8 @@ sources = [ # before the calls that end a detached thread, and this port's path for those # calls needs far more --- it overwrote the context table on macOS. "!musl/src/thread/__unmapself.c", + # Replaced in port/src/okm_thread.c: its main-thread branch reads the initial stack from the auxiliary vector. + "!musl/src/thread/pthread_getattr_np.c", "!musl/src/process/posix_spawn.c", # AND ITS SIBLING, WHICH IS EXCLUDED BECAUSE THE ONE ABOVE IS. # musl's posix_spawnp does not search a PATH: it stores `__execvpe' in the diff --git a/port/src/okm_thread.c b/port/src/okm_thread.c index fbaa87c..b583035 100644 --- a/port/src/okm_thread.c +++ b/port/src/okm_thread.c @@ -25,6 +25,7 @@ #include "okm_opt.h" #include +#include #include #include #include @@ -184,6 +185,13 @@ void __unmapself(void* base, size_t size) __syscall(SYS_exit, 0); } +/* openkal reports no bounds for the stack a context runs on. */ +int pthread_getattr_np(pthread_t t, pthread_attr_t* a) +{ + (void)t; (void)a; + return ENOSYS; +} + /* --- the suspension primitive ------------------------------------------------ */ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespec* t) From 50d1fad82f5990ca3a8b63cb4e98e4e04ab42c5c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 20:29:15 +0800 Subject: [PATCH 2/2] The two probes learn the exclusion, and the refusal is asserted as a refusal Continuous integration on this pull request was red before anything here, in `cross-link for the other system, from this one`: ld64.lld: error: duplicate symbol: _pthread_getattr_np >>> defined in musl_src_thread_pthread_getattr_np.c.o >>> defined in port_src_okm_thread.c.o Two scripts keep their OWN copy of the source set --- tools/probe-cross-macos.sh and tools/cross-build-macos.sh --- and both compile the directory rather than reading `sources` from mcpp.toml, so the exclusion this change adds there did not reach them. They carry the warning already: the comment in the probe records that this list went out of step when `posix_spawnp` became an exclusion and that the job is what said so. It has now said so a second time, and both lists carry the basename. It is stated at both copies because that is where a reader who adds the next exclusion is looking. THE CI ASSERTION NAMED A NUMBER THAT IS ONLY TRUE ON ONE ROW. It read `stack bounds: first 38, started 38`, which is the value of `ENOSYS` on Linux; on Darwin it is 78. The step has no `if:`, so it runs on the macOS row as well, where the program prints `first 78, started 78`, every observation in `examples/stack-bounds` still holds, and the step fails on the literal. It now asserts what the change is about --- that the enquiry is refused and that nothing else failed --- and leaves the errno comparison to the program, which makes it against the symbol. That is also the shape the steps beside it use. WHAT HAPPENS TO THE CALLER'S ATTRIBUTE IS NOW STATED AT THE DEFINITION. musl's version wrote the range into `*a` before returning; this one leaves it untouched on the error path, which is defensible and was silent. The comment now says which it is and why zeroing would be worse than leaving it: zero is a value the structure can legitimately hold, so a caller that ignored the return would read "no stack recorded" instead of "this call did not answer". THE EXCLUDED SOURCE IS RECORDED WHERE THE MANIFEST SAYS IT IS. The exclusion line points at musl/PATCHES.md for the reason, and the section listing replaced sources had no entry for it. It has one now, with the two wrong answers musl computed here and what a caller did with them. That section said the list was twelve while the manifest held eighteen exclusions before this change; the entry says to read the manifest for what is replaced and the section for why, rather than adding a nineteenth number that will go stale the same way. --- .github/workflows/ci.yml | 18 ++++++++++++++++-- musl/PATCHES.md | 18 +++++++++++++++++- port/src/okm_thread.c | 24 +++++++++++++++++++++++- tools/cross-build-macos.sh | 6 +++++- tools/probe-cross-macos.sh | 7 +++++-- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e51f834..8803bcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -780,12 +780,26 @@ jobs: bash tools/run-probe.sh examples/threads-detached threads-detached grep -q 'detached: 8 started, 8 ended' examples/threads-detached/run.log - - name: pthread_getattr_np reports that it cannot say + # AN ANSWER THAT IS REFUSED, WHICH IS NOT THE SAME AS AN ANSWER THAT IS + # ABSENT. musl's `pthread_getattr_np` derived a stack range from the + # auxiliary vector and from a thread's mapping; here the one is a static + # array and the other is a mapping no context runs on, so it described a + # range inside the program's own data and reported success. The port + # refuses it the way it already refuses `getrlimit(RLIMIT_STACK)`, and the + # probe walks to the boundary it is given, which is what an embedder does. + # musl/PATCHES.md, `src/thread/pthread_getattr_np.c`. + # + # THE ASSERTION NAMES THE REFUSAL AND NOT ITS NUMBER. `ENOSYS` is 38 on + # Linux and 78 on Darwin, and this step runs on both rows; a literal here + # would be a criterion that could only hold on one of them. The program + # compares against the symbol, and tools/run-probe.sh already holds both + # readings --- the count of failures, and that no line reports one. + - name: A stack's bounds are refused rather than invented env: MCPP_TARGET: ${{ matrix.target }} run: | bash tools/run-probe.sh examples/stack-bounds stack-bounds - grep -q 'stack bounds: first 38, started 38' examples/stack-bounds/run.log + grep -q -- '-- failures: 0 --' examples/stack-bounds/run.log # A LARGE ALLOCATION IS A MAPPING, AND A MAPPING IS WHOLE PAGES. # diff --git a/musl/PATCHES.md b/musl/PATCHES.md index 4d04ebd..961f218 100644 --- a/musl/PATCHES.md +++ b/musl/PATCHES.md @@ -130,7 +130,8 @@ carry it in a `long`. Twelve, and the list in the manifest carries the same reasons. Five read the shape of one environment directly. Two carry a machine word through a variable declared `long`. Three more were found only by running the result. And one is -replaced because another already was: +replaced because another already was. **The exclusions this port added later are +in that list too; read it for what is replaced, and this section for why.** `src/process/posix_spawnp.c` does not search a PATH. It stores `__execvpe` in the attributes and lets `posix_spawn` call it **in the duplicate** instead of @@ -163,6 +164,21 @@ table, so the exiting thread read its own record out of the bytes it had just written and jumped through them. `port/src/okm_thread.c` releases the mapping from the stack the thread is on; `examples/threads-detached` is the probe. +`src/thread/pthread_getattr_np.c` answers a question openkal does not carry, and +what it answered here was **wrong rather than absent**. For a context it started +it reports the mapping `pthread_create` allocated, which `__clone` ignores — the +context runs on the stack `kal_task_start` supplied. For the first context it +begins at `libc.auxv`, which this port points at a static array, and finds the +bottom by growing a mapping with `mremap`, which the dispatcher refuses with +`ENOSYS`: so it returned a page of the program's own data, as the top of the +stack, with a status of **success**. A caller that walked to the boundary it was +given walked off the stack it was on — WebAssembly Micro Runtime 2.4.5 did, and +died with `SIGSEGV` inside `wasm_runtime_init`. `port/src/okm_thread.c` returns +`ENOSYS` instead, which is both the truth and what this port already answers for +`getrlimit(RLIMIT_STACK)`; `examples/stack-bounds` asserts it for the first +context and for a started one, and the row in the README's absent table states +what a program observes. + `src/mman/mmap.c` returns a pointer through a `long`. It is replaced rather than patched because the replacement is also better where a `long` does hold a pointer: the value never becomes an integer at all. diff --git a/port/src/okm_thread.c b/port/src/okm_thread.c index b583035..d187fda 100644 --- a/port/src/okm_thread.c +++ b/port/src/okm_thread.c @@ -185,7 +185,29 @@ void __unmapself(void* base, size_t size) __syscall(SYS_exit, 0); } -/* openkal reports no bounds for the stack a context runs on. */ +/* openkal reports no bounds for the stack a context runs on. + * + * musl answers this from two places, and neither is true here. For a context it + * started it reports the mapping `pthread_create' allocated --- which `__clone' + * above ignores, so the context runs on the stack `kal_task_start' supplied. For + * the first context it derives the top of the stack from `libc.auxv', which this + * port points at a static array (`port/src/okm_start.c'), and finds the bottom by + * growing a mapping with `mremap' --- which the dispatcher refuses with `ENOSYS' + * (`port/src/okm_syscall.c'). What it returned was therefore a range inside the + * program's own data, reported as a success: a caller that walked to the boundary + * it was given walked off the stack it was on. + * + * ⇒ The refusal is the answer, and the form a caller reads it in is `ENOSYS', + * which is what `getrlimit(RLIMIT_STACK)' already answers here. Nothing is + * written to `*a': the enquiry has failed, and an attribute filled in anyway + * would be the same wrong range with a lighter warning. It is not zeroed either, + * because zero is a value this structure can legitimately hold, and a caller that + * ignored the return would then read "no stack recorded" rather than "this call + * did not answer". + * + * When openkal offers a way to learn the stack of the calling context, this + * function reports those bounds instead and musl's own source returns to the + * build. */ int pthread_getattr_np(pthread_t t, pthread_attr_t* a) { (void)t; (void)a; diff --git a/tools/cross-build-macos.sh b/tools/cross-build-macos.sh index a69580b..284b855 100755 --- a/tools/cross-build-macos.sh +++ b/tools/cross-build-macos.sh @@ -68,7 +68,11 @@ cd "$here" # Kept in step with mcpp.toml, INCLUDING that system's own exclusions: # okm_phdr.c answers dl_iterate_phdr from an ELF header and that format has none. -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' +# `pthread_getattr_np' is here for the same reason and is the second entry this +# list learned late; the compile below globs the directory, so a manifest +# exclusion that does not arrive here is a duplicate symbol rather than a +# mistake about which source runs. probe-cross-macos.sh states the rest. +skip='__libc_start_main|__init_tls|__set_thread_area|__unmapself|clone|pthread_getattr_np|posix_spawn|posix_spawnp|mmap|syscall_ret|getcwd|fcntl|dl_iterate_phdr|okm_phdr|cache' for f in musl/src/*/*.c musl/src/malloc/mallocng/*.c port/src/*.c port/src/*.S; do base=$(basename "$f"); base=${base%.*} [[ "$base" =~ ^($skip)$ ]] && continue diff --git a/tools/probe-cross-macos.sh b/tools/probe-cross-macos.sh index b446960..e6900d2 100755 --- a/tools/probe-cross-macos.sh +++ b/tools/probe-cross-macos.sh @@ -95,8 +95,11 @@ cd "$here" # Which is the whole reason this list carries the warning it does: it is a # SECOND statement of what mcpp.toml already states, and a second statement is # a thing that falls behind the first. It fell behind on the release that added -# the tenth entry, and it is this job that said so. -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' +# the tenth entry, and it is this job that said so --- and again on +# `pthread_getattr_np', with the same line in the log and that name in it. A +# reader who adds an exclusion to mcpp.toml adds the basename here in the same +# commit; the compiler below globs the directory, so nothing else will tell them. +skip='__libc_start_main|__init_tls|__set_thread_area|__unmapself|clone|pthread_getattr_np|posix_spawn|posix_spawnp|mmap|syscall_ret|getcwd|fcntl|dl_iterate_phdr|okm_phdr|cache' units=0 for f in musl/src/*/*.c musl/src/malloc/mallocng/*.c port/src/*.c port/src/*.S; do base=$(basename "$f"); base=${base%.*}