diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f75ed0..8803bcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -780,6 +780,27 @@ jobs: bash tools/run-probe.sh examples/threads-detached threads-detached grep -q 'detached: 8 started, 8 ended' examples/threads-detached/run.log + # 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 -- '-- failures: 0 --' 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/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 fbaa87c..d187fda 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,35 @@ void __unmapself(void* base, size_t size) __syscall(SYS_exit, 0); } +/* 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; + return ENOSYS; +} + /* --- the suspension primitive ------------------------------------------------ */ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespec* t) 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%.*}