Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
13 changes: 13 additions & 0 deletions examples/stack-bounds/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
36 changes: 36 additions & 0 deletions examples/stack-bounds/src/main.c
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#include <pthread.h>
#include <stdio.h>

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;
}
3 changes: 3 additions & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion musl/PATCHES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions port/src/okm_thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "okm_opt.h"

#include <errno.h>
#include <pthread.h>
#include <setjmp.h>
#include <stdint.h>
#include <string.h>
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion tools/cross-build-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions tools/probe-cross-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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%.*}
Expand Down
Loading