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
38 changes: 24 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. |
Expand Down
106 changes: 94 additions & 12 deletions examples/stack-bounds/src/main.c
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>

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;
}
30 changes: 23 additions & 7 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
#
Expand All @@ -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.
#
Expand All @@ -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.
#
Expand Down
Loading
Loading