From e090a75b1bf441e949595337d367e881e825b56d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 20:26:13 +0800 Subject: [PATCH 1/2] The review of a proposal is recorded beside the package, and is not part of it --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 04c0e84..de74289 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,10 @@ build.log # sources. No trailing slash: the pattern must match a symbolic link as well. .spec .impl + +# What a review of a change leaves behind. A review is a reading of one +# proposal at one moment, by one reviewer; it is not a fact about the package, +# and the proposal it describes is not in this tree. The notes under +# `.agents/docs/` are kept because they state design decisions this repository +# stands behind; these are not. +.agents/docs/reviews/ From b796e6c763b214a68940aed9b278eb705587df1f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 2 Oct 2026 23:53:53 +0800 Subject: [PATCH 2/2] 0.20.0 --- a context can say where it stands, so the C library stops refusing 0.19.3 answered `pthread_getattr_np` with `ENOSYS` for every thread, because musl's own answer was wrong in both directions: for the first context it began at the auxiliary vector, which this port points at a static array, and found a bottom by growing a mapping the dispatcher refuses; for a started one it named the mapping `pthread_create` allocated, which `kal_task_start` supplies the stack instead of. A caller that trusted either walked off the stack it was on. That was honest and was not an answer. OPENKAL 0.15 LETS A RUNNING CONTEXT SAY WHERE IT STANDS, so the port answers with the region the CALLING thread is actually on --- `kal_task_stack`, measured by the implementation beneath --- and refuses for any other thread, uniformly and never with a range. The refusal is not a gap left here: a context can only be asked about itself, because a handle is meaningful only to the party that obtained it and there is no handle to a context at all. `ENOSYS` is also what an implementation that cannot measure produces, and it is what `getrlimit(RLIMIT_STACK)` already answers here. WHAT IS WRITTEN TO THE ATTRIBUTE IS MUSL'S OWN ARRANGEMENT: the stack address is the high end and the size the distance down from it, which is what `pthread_attr_getstack` subtracts to answer POSIX's lowest usable address. The guard size is zero because the region reported has no guard inside it, and on failure nothing is written at all --- not even zeroed, because zero is a value this structure can legitimately hold and a caller that ignored the return would read "no stack recorded" rather than "this call did not answer". The `[c-abi-absent]` row is WITHDRAWN with its reasoning left where the row was, and the README's absent-table row with it: no single form describes the call as a whole, since `enosys` would be false of the common spelling and `accepted-no-effect` false of the general case. The assertion moved to the example that reads it rather than being dropped. `examples/stack-bounds` now asserts CONTAINMENT --- the region contains a local of the context that asked, for the first context and for a started one, and the two are not the same region --- and asserts the refusal for another thread, whose identifier is already joined and is compared rather than read. Numbers are not compared: lengths and addresses are the system's, and containment is the property both wrong answers violated. Measured here on x86_64 Linux against openkal-linux 0.16.0: the first context is reported a 3280896-byte region containing its own stack, a started context the 262144-byte region it was allocated, and another thread is refused. --- .github/workflows/ci.yml | 38 +++++++---- README.md | 4 +- examples/stack-bounds/src/main.c | 106 +++++++++++++++++++++++++++---- mcpp.toml | 30 +++++++-- musl/PATCHES.md | 35 ++++++---- port/src/okm_thread.c | 68 +++++++++++++------- 6 files changed, 212 insertions(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8803bcd..e9a359f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -780,25 +780,35 @@ 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`. + # A CONTEXT IS TOLD WHERE IT STANDS, AND ANOTHER THREAD IS STILL REFUSED. # - # 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 + # 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. 0.19.3 replaced that with a + # refusal, which was honest and was not an answer. + # + # openkal 0.15 lets a running context say where it stands, so the port now + # answers with the region the CALLING thread is on --- musl's own source + # stays excluded, and port/src/okm_thread.c states why both of its + # branches are wrong here --- and refuses for any other thread rather than + # describing one it cannot see. musl/PATCHES.md, + # `src/thread/pthread_getattr_np.c`. + # + # THE EXAMPLE IS THE ASSERTION: the region must contain a local of the + # context that asked --- for the first context and for a started one --- + # and another thread must be refused. Numbers are not compared, because + # lengths and addresses are the system's; containment is the property a + # caller relies on, and it is what both wrong answers violated. The + # refusal is compared against the SYMBOL and not against a literal: + # `ENOSYS` is 38 on Linux and 78 on Darwin, and this step runs on both. + - name: A context is told where it stands, and another thread is refused env: MCPP_TARGET: ${{ matrix.target }} run: | bash tools/run-probe.sh examples/stack-bounds stack-bounds + grep -q -- 'contained=1' examples/stack-bounds/run.log + grep -q -- 'another thread refused=1' 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/README.md b/README.md index f44ec31..fd0814d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ the claim can be checked rather than repeated. ```toml [dependencies] -openkal-musl = "0.19.3" +openkal-musl = "0.20.0" ``` > **Engine floor (mcpp 2026.9.18.3):** this version of this package declares @@ -295,7 +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. | +| ~~a stack's bounds~~ | **answered since 0.20.0** — `pthread_getattr_np` reports the region the CALLING thread is on, and refuses (`ENOSYS`) for any other thread | openkal 0.15 states where a running context stands (`kal_task_stack`), and the port answers from it. What musl computed was wrong in both directions: for the main thread it began at the auxiliary vector, which here is a static array, and for a thread it named the mapping `pthread_create` allocated, which `kal_task_start` supplies the stack instead of. The refusal remains for another thread, because a context can only be asked about itself — never a range composed for one this port cannot see. | | 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/src/main.c b/examples/stack-bounds/src/main.c index f263b23..f867206 100644 --- a/examples/stack-bounds/src/main.c +++ b/examples/stack-bounds/src/main.c @@ -1,36 +1,118 @@ -/* pthread_getattr_np reports that it cannot say, for the first context and for a started one. +/* Where the context that asks actually stands. * - * 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. + * musl's pthread_getattr_np was wrong here in both directions, and this example + * is the reading of what replaced it. For the first context musl began at the + * auxiliary vector, which this port points at a static array, and found a bottom + * by growing a mapping the dispatcher refuses; for a started one it named the + * mapping pthread_create allocated, which openkal's kal_task_start supplies the + * stack instead of. A caller that trusted either walked off the stack it was on. + * + * THE PROPERTY IS CONTAINMENT AND NOT A NUMBER. The address of a local is on the + * calling context's own stack, so it must lie inside the region the call + * reports --- for the first context, for a started one, and for a context that + * has used its stack. Lengths and addresses differ between systems and between + * builds; that the range contains the context that asked is what a caller relies + * on. + * + * AND THE GENERAL CASE IS STILL REFUSED. `pthread_getattr_np(t, ...)' for a + * thread other than the caller has no answer above openkal, because a context + * can only be asked about itself. The refusal is asserted here, because a + * refusal a program can read is what keeps a wrong range from being returned + * instead. */ #define _GNU_SOURCE #include #include +#include #include +static int holds(void* base, size_t size, void* here) +{ + const uintptr_t b = (uintptr_t)base; + const uintptr_t at = (uintptr_t)here; + return size != 0 && b + size > b && at >= b && at - b < size; +} + +struct started { + int reported; /* the return of the enquiry inside the context */ + void* base; + size_t size; + int contained; /* whether the region held a local of that context */ +}; + static void* body(void* arg) { + struct started* s = arg; + char here = 0; + pthread_attr_t a; - *(int*)arg = pthread_getattr_np(pthread_self(), &a); + int e = pthread_getattr_np(pthread_self(), &a); + void* base = 0; + size_t size = 0; + if (e == 0 && pthread_attr_getstack(&a, &base, &size) != 0) e = -1; + + s->reported = e; + s->base = base; + s->size = size; + s->contained = e == 0 && holds(base, size, &here); return 0; } int main(void) { int failures = 0; + + /* The first context: the region contains the context that asked. */ + char here = 0; pthread_attr_t a; - int first = pthread_getattr_np(pthread_self(), &a); - int started = 0; + void* base = 0; + size_t size = 0; + int e = pthread_getattr_np(pthread_self(), &a); + if (e != 0) { + printf("FAIL: the first context was refused (%d)\n", e); + ++failures; + } else if (pthread_attr_getstack(&a, &base, &size) != 0) { + puts("FAIL: the reported attribute was not readable"); + ++failures; + } else if (!holds(base, size, &here)) { + printf("FAIL: %p is not inside [%p, %p)\n", (void*)&here, base, + (void*)((uintptr_t)base + size)); + ++failures; + } + printf("stack bounds: first e=%d base=%p size=%lu contained=%d\n", e, base, + (unsigned long)size, holds(base, size, &here)); + + /* A started context: the same, about itself, and not about the first one. */ + struct started s = { 0, 0, 0, 0 }; pthread_t t; - if (pthread_create(&t, 0, body, &started) != 0 || pthread_join(t, 0) != 0) { + if (pthread_create(&t, 0, body, &s) != 0 || pthread_join(t, 0) != 0) { puts("FAIL: the thread did not run"); ++failures; + } else { + if (s.reported != 0) { + printf("FAIL: the started context was refused (%d)\n", s.reported); + ++failures; + } + if (!s.contained) { + puts("FAIL: the started context's region is not its own"); + ++failures; + } + if (s.base == base && s.size == size) { + puts("FAIL: the started context was told the first context's region"); + ++failures; + } + printf("stack bounds: started e=%d base=%p size=%lu contained=%d\n", + s.reported, s.base, (unsigned long)s.size, s.contained); } - 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; } + + /* Another thread is refused rather than described, and the identifier used + * here has already been joined: the port compares it and never reads it, so + * a refusal is the only thing this call can produce. */ + pthread_attr_t other; + int refused = pthread_getattr_np(t, &other) == ENOSYS; + if (!refused) { puts("FAIL: another thread was answered rather than refused"); ++failures; } + printf("stack bounds: another thread refused=%d\n", refused); + printf("-- failures: %d --\n", failures); return failures == 0 ? 0 : 1; } diff --git a/mcpp.toml b/mcpp.toml index 9f74296..46ac006 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-musl" -version = "0.19.3" +version = "0.20.0" description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel." license = "Apache-2.0" @@ -117,7 +117,23 @@ 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" } +# WITHDRAWN IN 0.20.0, AND THIS IS WHERE ITS ROW WAS: `pthread_getattr_np'. +# +# 0.19.3 declared the name `enosys', because musl's own answer described a range +# that was not the stack --- a page of this port's static auxiliary vector for +# the first context, and a mapping no context runs on for a started one --- and a +# refusal was the only honest answer then available. +# +# openkal 0.15 lets a context say where it stands, so the port now ANSWERS for +# the calling thread with the region it is actually on, and refuses for any other +# thread rather than describing one it cannot see. No single form describes the +# call as a whole: `enosys' would be false of the common spelling, +# `accepted-no-effect' false of the general case, and `link' false of both. +# +# THE ASSERTION MOVED TO THE EXAMPLE THAT READS IT rather than being dropped: +# examples/stack-bounds checks the reported region against the caller's own +# stack, checks that a started context is told its own, and checks that another +# thread is refused. # 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 @@ -147,7 +163,7 @@ pthread_getattr_np = { form = "enosys", note = "openkal reports no bounds for th tcsetattr = { form = "accepted-no-effect", note = "openkal names three positions of the terminal mode word; a request that alters output post-processing, the line speed, the control characters or VMIN/VTIME is accepted and that part has no effect" } [dependencies] -openkal = "0.14.1" +openkal = "0.15.0" # An ordinary consumer of openkal declares the specification and leaves the # choice of implementation to whoever builds the program, which is what the @@ -162,7 +178,7 @@ openkal = "0.14.1" # # The consequence for a program is that it names this package and nothing else. [target.'cfg(os = "linux")'.dependencies] -openkal-linux = { version = "0.15.1", features = ["standalone"] } +openkal-linux = { version = "0.16.0", features = ["standalone"] } [target.'cfg(os = "macos")'.dependencies] # 0.12.0 is the first release of this implementation that declares which @@ -172,7 +188,7 @@ openkal-linux = { version = "0.15.1", features = ["standalone"] } # is the shape of a check that silently does not run --- while the same # consumer was answered on Linux and Windows, whose implementations have # carried the array since openkal-linux 0.15.0 and openkal-windows 0.10.0. -openkal-macos = { version = "0.12.1", features = ["standalone"] } +openkal-macos = { version = "0.13.0", features = ["standalone"] } # FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT. # @@ -183,7 +199,7 @@ openkal-macos = { version = "0.12.1", features = ["standalone"] } # runtime that receives control, and a C library configured for an environment # with no process to exit from. So this declares the implementation and stops. [target.'cfg(os = "none")'.dependencies] -openkal-opensbi = { version = "0.8.1", features = ["standalone"] } +openkal-opensbi = { version = "0.8.2", features = ["standalone"] } # WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH IS EXPECTED TO PROVIDE. # @@ -208,7 +224,7 @@ openkal-opensbi = { version = "0.8.1", features = ["standalone"] } defines = ["OKM_HAS_FS=0", "OKM_HAS_PROCESS=0", "OKM_HAS_TASK=0"] [target.'cfg(windows)'.dependencies] -openkal-windows = { version = "0.10.2", features = ["standalone"] } +openkal-windows = { version = "0.11.0", features = ["standalone"] } # The feature macros musl's own build establishes. # diff --git a/musl/PATCHES.md b/musl/PATCHES.md index 961f218..fdbfb9f 100644 --- a/musl/PATCHES.md +++ b/musl/PATCHES.md @@ -164,20 +164,31 @@ 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 +`src/thread/pthread_getattr_np.c` answers a question openkal did not carry until +0.15, 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. +died with `SIGSEGV` inside `wasm_runtime_init`. + +**What replaced it changed once, and the change is the point of this entry.** +0.19.3 answered `ENOSYS` for every thread, which was honest and was not an +answer. openkal 0.15 states where a running context stands (`kal_task_stack`, +clause 11 entry 21), so `port/src/okm_thread.c` now answers for the **calling** +thread from that region — the one the context is actually on, not the mapping +`pthread_create` allocated — and refuses with `ENOSYS` for any other thread, +uniformly and never with a range. The refusal is not a gap left by this port: a +context can only be asked about itself, because openkal's handles are meaningful +only to the party that obtained them, and there is no handle to a context at all. + +`examples/stack-bounds` asserts containment — the region contains a local of the +context that asked, for the first context and for a started one — and asserts +the refusal for another thread. Musl's own source stays excluded: both of its +branches remain wrong here, and restoring it would require correcting it. `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 diff --git a/port/src/okm_thread.c b/port/src/okm_thread.c index d187fda..a703d0c 100644 --- a/port/src/okm_thread.c +++ b/port/src/okm_thread.c @@ -31,6 +31,7 @@ #include #include #include "futex.h" +#include "pthread_impl.h" #include "syscall.h" void __okm_set_tp(uintptr_t value); @@ -185,33 +186,56 @@ void __unmapself(void* base, size_t size) __syscall(SYS_exit, 0); } -/* openkal reports no bounds for the stack a context runs on. +/* The region a context runs on, and the one question musl asks about it. * - * 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. + * MUSL'S OWN ANSWER IS WRONG HERE IN TWO DIRECTIONS, which is why the source is + * excluded and this is written instead. For a context it started, musl reports + * the mapping `pthread_create' allocated --- and `__clone' above ignores that + * mapping, because kal_task_start supplies the stack, so the range it names is + * one the context never runs on. For the first context it derives a top from + * `libc.auxv', which this port points at a static array, and computes a bottom + * by growing a mapping with `mremap', which the dispatcher refuses: what it + * reported was a range inside the program's own data, 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". + * SO THE ANSWER COMES FROM openkal, WHICH MEASURES THE MAPPING THE CONTEXT IS + * ACTUALLY ON (openkal 0.15, `kal_task_stack'), AND IT IS ANSWERED FOR THE + * CALLING THREAD ONLY. A context that is running can say where it stands; a + * thread this port was not asked about cannot be asked at all, because openkal + * has no operation that takes a context and the record of one is not this + * port's to keep. `ENOSYS' is the answer there --- the same shape the absence + * of the capability had before --- and never a range: a caller that reads the + * return is told the truth, and a caller that does not is handed nothing. * - * 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. */ + * WHAT IS WRITTEN AND WHAT IS NOT. On success the attribute is zeroed and three + * fields are filled, in the arrangement musl reads: the stack ADDRESS is the + * high end and the size is the distance down from it, which is what + * `pthread_attr_getstack' subtracts to answer POSIX's lowest usable address. + * The guard size is zero because the region openkal reports has no guard inside + * it --- musl's guard belongs to the mapping it allocated and nothing runs + * there. On failure nothing is written at all, not even zeroed: zero is a value + * this structure can legitimately hold, and a caller that ignored the return + * would read "no stack recorded" rather than "this call did not answer". + * + * WHEN THIS PORT RUNS ON AN IMPLEMENTATION THAT CANNOT MEASURE, THE ENQUIRY + * FAILS AND THIS REPORTS `ENOSYS' --- which is the honest answer and is what + * `getrlimit(RLIMIT_STACK)' answers here as well. */ int pthread_getattr_np(pthread_t t, pthread_attr_t* a) { - (void)t; (void)a; - return ENOSYS; + if (a == 0) return EINVAL; + if (t != __pthread_self()) return ENOSYS; + + void* base = 0; + kal_uintptr size = 0; + if (kal_task_stack(&base, &size) != kal_ok) return ENOSYS; + if (base == 0 || size == 0) return ENOSYS; + + *a = (pthread_attr_t){0}; + a->_a_detach = __pthread_self()->detach_state >= DT_DETACHED; + a->_a_guardsize = 0; + a->_a_stackaddr = (uintptr_t)base + size; + a->_a_stacksize = size; + return 0; } /* --- the suspension primitive ------------------------------------------------ */