pthread_getattr_np reports ENOSYS - #49
Merged
Merged
Conversation
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.
…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.
Sunrisepeak
force-pushed
the
pthread-getattr-np-enosys
branch
from
October 2, 2026 12:31
144357b to
50d1fad
Compare
Sunrisepeak
added a commit
to mcpplibs/openkal-linux
that referenced
this pull request
Oct 2, 2026
`kal_task_stack` reports the stack of the CALLING context. Every context this implementation creates runs on a region it allocated, so the pair is recorded when the context begins and returned as it stands; the context the program was started on has no such record, and its region is measured. RLIMIT_STACK ALONE IS NOT THE ANSWER, AND BELIEVING IT WAS WOULD BE THE DEFECT THIS EXISTS TO REMOVE. The limit is a policy, and the floor the kernel enforces is `mapping_end - RLIMIT_STACK` --- the end of the MAPPING, which is not the address the program's first instruction sees. `setup_arg_pages` places the arguments at the top of the mapping and moves the mapping's start down by a random shift, so a bound computed from a stack pointer begins below the real floor. Below the real floor is where this system puts the program's own libraries, so the range would contain another mapping: a caller that laid a guard at its base, or measured the room above it, would be measuring a region the kernel will not grow the stack into. So the mapping is measured with `mincore` --- an exponential-then-bisection search outward from a local of the caller --- and the floor is the higher of the two bounds the kernel itself applies: `mapping_end - RLIMIT_STACK`, read with `prlimit64`, and one guard gap above the nearest mapping below. BOTH BOUNDS WERE MEASURED ON 6.8.0 BEFORE BEING WRITTEN DOWN. A descending write stopped at exactly `mapping_end - RLIMIT_STACK`, with one page below it unwritable and one page above it writable; with one page mapped by the test itself inside the reservation, the same write stopped at exactly that page's end plus 256 pages. THE GUARD GAP IS A KERNEL GLOBAL THIS IMPLEMENTATION CANNOT READ, and the default is therefore what is applied. A kernel booted with a larger gap stops the stack above the base reported here --- the region is reported larger than the kernel will grow it, which is the direction a caller must not be wrong in, and the reason the default is applied rather than a smaller number. A kernel booted with a smaller gap is reported a region smaller than it could have, which is the harmless direction. Both are stated at the code. An environment that will not answer --- `mincore` refused by a filter, say --- is reported as the kernel's condition rather than as a range, because a range derived from a refusal is a range derived from nothing. It is not reported as "unsupported": clause 6.1 forbids a provided interface to answer that way. Where the program carries a runtime of its own, that runtime owns the stacks and is asked instead (`pthread_getattr_np`), which is the same answer a program above openkal would get for itself. That is the one arrangement in which this implementation may take a C library's name, and the file says so. The conformance suite observes containment --- the reported region contains a local of the context that asked, for the first context and for a started one, and the answer does not change while the context runs --- and this package's own tests observe the same property, because it is the one both of the wrong answers mcpplibs/openkal-musl#49 removed had violated. AND A LATENT DEFECT IN THE HOSTED ARRANGEMENT WAS EXPOSED BY IT, AND IS FIXED HERE RATHER THAN LEFT FOR THE NEXT LARGER VARIABLE. `describe_tls` was called only by this implementation's own entry point, so a program that carries a runtime never described its thread-local image: a started context was given a region laid out from a segment of size zero, its thread pointer sat at the START of that region rather than at its end, and every thread-local variable of that context was addressed below the allocation --- into whatever the allocator kept beside it. Nothing faulted, because that memory is usually mapped. It was invisible while this implementation's own storage was one four-byte variable and fatal once it was twenty-four: the specification package's own kit test died at the first instruction of a context (measured 2026-10-03). `make_tls` now describes the image if nothing has, and refuses rather than laying out a region it cannot size. The two alignments are also separated, which is the second half of the same defect: the linker measures every offset from `round_up(memsz, p_align)`, and clamping that to sixteen --- which the ALLOCATION wants --- moves every declared value a few bytes away from the variable declared with it. It is invisible until a thread-local variable is declared with a non-zero value, which is now what tests/conformance_task_tls.cpp examines, alongside the region's size and the isolation between contexts.
Sunrisepeak
added a commit
to mcpplibs/openkal
that referenced
this pull request
Oct 2, 2026
kal_task_stack reports the stack of the CALLING context, and it takes no handle. A handle is meaningful in the context of the party that obtained it (clause 7.2) and kal_task_current reports an identity rather than a handle, so a context is the one resource its own code always stands on and can never hold --- an enquiry taking a context would have to answer for a context the implementation does not control, which is the registry clause 7.1 excludes. IT IS ADMITTED BECAUSE EVERY RESOURCE ANSWERS, which is what clause 6.4 asks. A running context can find its own stack in every environment the interface is provided for: Linux states the limit it grows the mapping against, macOS answers for the calling thread, Windows answers the bounds it built the thread's stack from, and the context the program was STARTED on is the easiest case rather than the hardest because the first openkal code is already running on it. An environment that cannot answer does not provide openkal.task, which clause 3 already expresses as an absence at the link --- openkal-emscripten does exactly that today, and openkal-opensbi provides none of the interface. The size is the region the implementation vouches for, and where an environment separates a reservation from what is committed it is the reservation: a caller places a guard below the base or measures the room above it, and a bound that moved as the stack grew would answer the second question wrongly and the first one not at all. Asked for by the C library port, whose pthread_getattr_np had no honest answer above an implementation of openkal.task: musl's implementation described a range that was not the stack, and mcpplibs/openkal-musl#49 replaced both of its wrong branches with a refusal. A refusal is correct and is still not an answer. The conformance suite observes the property a caller relies on rather than the shape of the numbers: the range reported to a context contains that context's own stack, for the context that was started on and for one that was started by it, and it does not change while the context runs.
Sunrisepeak
added a commit
to mcpplibs/openkal-linux
that referenced
this pull request
Oct 2, 2026
`kal_task_stack` reports the stack of the CALLING context. Every context this implementation creates runs on a region it allocated, so the pair is recorded when the context begins and returned as it stands; the context the program was started on has no such record, and its region is measured. RLIMIT_STACK ALONE IS NOT THE ANSWER, AND BELIEVING IT WAS WOULD BE THE DEFECT THIS EXISTS TO REMOVE. The limit is a policy, and the floor the kernel enforces is `mapping_end - RLIMIT_STACK` --- the end of the MAPPING, which is not the address the program's first instruction sees. `setup_arg_pages` places the arguments at the top of the mapping and moves the mapping's start down by a random shift, so a bound computed from a stack pointer begins below the real floor. Below the real floor is where this system puts the program's own libraries, so the range would contain another mapping: a caller that laid a guard at its base, or measured the room above it, would be measuring a region the kernel will not grow the stack into. So the mapping is measured with `mincore` --- an exponential-then-bisection search outward from a local of the caller --- and the floor is the higher of the two bounds the kernel itself applies: `mapping_end - RLIMIT_STACK`, read with `prlimit64`, and one guard gap above the nearest mapping below. BOTH BOUNDS WERE MEASURED ON 6.8.0 BEFORE BEING WRITTEN DOWN. A descending write stopped at exactly `mapping_end - RLIMIT_STACK`, with one page below it unwritable and one page above it writable; with one page mapped by the test itself inside the reservation, the same write stopped at exactly that page's end plus 256 pages. THE GUARD GAP IS A KERNEL GLOBAL THIS IMPLEMENTATION CANNOT READ, and the default is therefore what is applied. A kernel booted with a larger gap stops the stack above the base reported here --- the region is reported larger than the kernel will grow it, which is the direction a caller must not be wrong in, and the reason the default is applied rather than a smaller number. A kernel booted with a smaller gap is reported a region smaller than it could have, which is the harmless direction. Both are stated at the code. An environment that will not answer --- `mincore` refused by a filter, say --- is reported as the kernel's condition rather than as a range, because a range derived from a refusal is a range derived from nothing. It is not reported as "unsupported": clause 6.1 forbids a provided interface to answer that way. Where the program carries a runtime of its own, that runtime owns the stacks and is asked instead (`pthread_getattr_np`), which is the same answer a program above openkal would get for itself. That is the one arrangement in which this implementation may take a C library's name, and the file says so. The conformance suite observes containment --- the reported region contains a local of the context that asked, for the first context and for a started one, and the answer does not change while the context runs --- and this package's own tests observe the same property, because it is the one both of the wrong answers mcpplibs/openkal-musl#49 removed had violated. AND A LATENT DEFECT IN THE HOSTED ARRANGEMENT WAS EXPOSED BY IT, AND IS FIXED HERE RATHER THAN LEFT FOR THE NEXT LARGER VARIABLE. `describe_tls` was called only by this implementation's own entry point, so a program that carries a runtime never described its thread-local image: a started context was given a region laid out from a segment of size zero, its thread pointer sat at the START of that region rather than at its end, and every thread-local variable of that context was addressed below the allocation --- into whatever the allocator kept beside it. Nothing faulted, because that memory is usually mapped. It was invisible while this implementation's own storage was one four-byte variable and fatal once it was twenty-four: the specification package's own kit test died at the first instruction of a context (measured 2026-10-03). `make_tls` now describes the image if nothing has, and refuses rather than laying out a region it cannot size. The two alignments are also separated, which is the second half of the same defect: the linker measures every offset from `round_up(memsz, p_align)`, and clamping that to sixteen --- which the ALLOCATION wants --- moves every declared value a few bytes away from the variable declared with it. It is invisible until a thread-local variable is declared with a non-zero value, which is now what tests/conformance_task_tls.cpp examines, alongside the region's size and the isolation between contexts.
Sunrisepeak
added a commit
to mcpplibs/openkal-linux
that referenced
this pull request
Oct 2, 2026
…red (#32) `kal_task_stack` reports the stack of the CALLING context. Every context this implementation creates runs on a region it allocated, so the pair is recorded when the context begins and returned as it stands; the context the program was started on has no such record, and its region is measured. RLIMIT_STACK ALONE IS NOT THE ANSWER, AND BELIEVING IT WAS WOULD BE THE DEFECT THIS EXISTS TO REMOVE. The limit is a policy, and the floor the kernel enforces is `mapping_end - RLIMIT_STACK` --- the end of the MAPPING, which is not the address the program's first instruction sees. `setup_arg_pages` places the arguments at the top of the mapping and moves the mapping's start down by a random shift, so a bound computed from a stack pointer begins below the real floor. Below the real floor is where this system puts the program's own libraries, so the range would contain another mapping: a caller that laid a guard at its base, or measured the room above it, would be measuring a region the kernel will not grow the stack into. So the mapping is measured with `mincore` --- an exponential-then-bisection search outward from a local of the caller --- and the floor is the higher of the two bounds the kernel itself applies: `mapping_end - RLIMIT_STACK`, read with `prlimit64`, and one guard gap above the nearest mapping below. BOTH BOUNDS WERE MEASURED ON 6.8.0 BEFORE BEING WRITTEN DOWN. A descending write stopped at exactly `mapping_end - RLIMIT_STACK`, with one page below it unwritable and one page above it writable; with one page mapped by the test itself inside the reservation, the same write stopped at exactly that page's end plus 256 pages. THE GUARD GAP IS A KERNEL GLOBAL THIS IMPLEMENTATION CANNOT READ, and the default is therefore what is applied. A kernel booted with a larger gap stops the stack above the base reported here --- the region is reported larger than the kernel will grow it, which is the direction a caller must not be wrong in, and the reason the default is applied rather than a smaller number. A kernel booted with a smaller gap is reported a region smaller than it could have, which is the harmless direction. Both are stated at the code. An environment that will not answer --- `mincore` refused by a filter, say --- is reported as the kernel's condition rather than as a range, because a range derived from a refusal is a range derived from nothing. It is not reported as "unsupported": clause 6.1 forbids a provided interface to answer that way. Where the program carries a runtime of its own, that runtime owns the stacks and is asked instead (`pthread_getattr_np`), which is the same answer a program above openkal would get for itself. That is the one arrangement in which this implementation may take a C library's name, and the file says so. The conformance suite observes containment --- the reported region contains a local of the context that asked, for the first context and for a started one, and the answer does not change while the context runs --- and this package's own tests observe the same property, because it is the one both of the wrong answers mcpplibs/openkal-musl#49 removed had violated. AND A LATENT DEFECT IN THE HOSTED ARRANGEMENT WAS EXPOSED BY IT, AND IS FIXED HERE RATHER THAN LEFT FOR THE NEXT LARGER VARIABLE. `describe_tls` was called only by this implementation's own entry point, so a program that carries a runtime never described its thread-local image: a started context was given a region laid out from a segment of size zero, its thread pointer sat at the START of that region rather than at its end, and every thread-local variable of that context was addressed below the allocation --- into whatever the allocator kept beside it. Nothing faulted, because that memory is usually mapped. It was invisible while this implementation's own storage was one four-byte variable and fatal once it was twenty-four: the specification package's own kit test died at the first instruction of a context (measured 2026-10-03). `make_tls` now describes the image if nothing has, and refuses rather than laying out a region it cannot size. The two alignments are also separated, which is the second half of the same defect: the linker measures every offset from `round_up(memsz, p_align)`, and clamping that to sixteen --- which the ALLOCATION wants --- moves every declared value a few bytes away from the variable declared with it. It is invisible until a thread-local variable is declared with a non-zero value, which is now what tests/conformance_task_tls.cpp examines, alongside the region's size and the isolation between contexts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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. For the first context it was a page of the port's static auxiliary vector, because musl rounds libc.auxv up to a page, takes that as the top of the stack and finds the bottom by calling mremap, which answers ENOSYS here. For a context started by pthread_create it was the mapping pthread_create allocated, which the context never runs on, since __clone ignores the stack it is given.
This excludes musl's pthread_getattr_np.c and defines a replacement in port/src/okm_thread.c that returns ENOSYS, the answer getrlimit(RLIMIT_STACK) already gives. The name is added to [c-abi-absent] as an enosys row and to the README table of absent facilities, and examples/stack-bounds checks that both the first context and a started one get the error. A caller that checks the return value can handle an error; it cannot tell that the range it was given is wrong. WebAssembly Micro Runtime 2.4.5, for example, asks for the stack boundary and then touches the stack down to it, so with the old answer it died with SIGSEGV inside wasm_runtime_init; with this change wasm_runtime_full_init prints "Failed to init stack guard pages" and returns false.
If openkal later offers a way to learn the stack of the calling context, the function can return the real bounds instead.
Fixes #47.
Checked on the 0.19.2 tree, which differs from main only in version pins, on x86_64-linux-musl (debug and release) and on aarch64-linux-musl under qemu-aarch64, with llvm 22.1.8: examples/stack-bounds prints "stack bounds: first 38, started 38" and reports no failures with this change, and reports two failures (both calls return 0) without it. examples/threads-cxx and examples/threads-detached still pass, and tools/check-absent.sh agrees with all 25 rows. I could not run the macOS and Windows rows of the CI matrix.