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
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-macos"
version = "0.8.0"
version = "0.9.0"
description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used."
license = "Apache-2.0"

Expand All @@ -18,7 +18,7 @@ authors = ["mcpplibs"]
repo = "https://github.com/mcpplibs/openkal-macos"

[dependencies]
openkal = "0.11.0"
openkal = "0.12.0"

[build]
# The flags are attached to this package's own sources rather than to the whole
Expand Down
134 changes: 132 additions & 2 deletions src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,121 @@ int kal_process_terminate(kal_process h) {
// ⇒ Null, and KAL_PROCESS_PROP_STOP_REQUESTED unclaimed, so a caller that asks
// first is told. The other implementation answers it; this one will when it can
// be exercised here.
const kal_u32* kal_process_stop_requested(void) { return 0; }
// ⭐⭐ A WORD THIS PROGRAM'S ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED IT TO END.
//
// ⚠️⚠️ THE TRAMPOLINE IS THIS IMPLEMENTATION'S OWN, WHICH IS WHY THIS ARRIVED A
// VERSION LATE. The raw `sigaction' of this kernel takes a structure whose
// SECOND field is `sa_tramp': the kernel enters that address, not the handler,
// and the handler is passed to it as an argument. A C library ordinarily
// supplies it (`_sigtramp' in libsystem) and there is no C library beneath this
// implementation. A structure installed with a null or wrong `sa_tramp' does not
// fail at installation --- it fails on DELIVERY, inside the handler, at an
// address that belongs to nobody, which is the least attributable failure this
// implementation could ship. So the order was: a conformance check that raises
// the signal first, this second.
//
// ⚠️ ARMED ON THE FIRST ENQUIRY AND NOT AT STARTUP, exactly as openkal-linux
// argues: a program that never asks keeps the default action, and adding this
// operation therefore changes nothing for anyone who does not use it.
namespace {

kal_u32 g_stop_word = 0;
int g_stop_armed = 0;

#if defined(__aarch64__)

constexpr okm_long nr_sigaction = 46;

// What the kernel enters. Its arguments are (handler, infostyle, sig, siginfo,
// ucontext) in x0..x4; it calls the handler with the last three and then asks
// the kernel to restore the interrupted context.
//
// ⚠️ x19 AND x20 ARE USED WITHOUT BEING SAVED, and that is correct here rather
// than sloppy: this function does not return to its caller. `sigreturn' restores
// the whole of the interrupted context, callee-saved registers included, so the
// values these two held belong to a frame the kernel is about to reinstate.
extern "C" void okm_sigtramp(void);
asm(".globl _okm_sigtramp\n"
".p2align 2\n"
"_okm_sigtramp:\n"
" mov x19, x1\n" // infostyle
" mov x20, x4\n" // ucontext
" mov x8, x0\n" // handler
" mov x0, x2\n" // sig
" mov x1, x3\n" // siginfo
" mov x2, x20\n" // ucontext
" blr x8\n"
" mov x0, x20\n" // ucontext
" mov x1, x19\n" // infostyle
" mov x16, #184\n" // SYS_sigreturn
" svc #0x80\n"
" brk #1\n"); // sigreturn does not come back

void stop_handler(int) {
__atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE);
// Woken through the same operation `kal_task_wake' performs, issued as the
// raw call because a handler may not enter code that takes a lock. Waking
// ALL of them: any number of contexts may be waiting upon this one word, and
// the handler has no way to learn how many.
okm::sys(okm::nr_ulock_wake,
okm::ul_compare_and_wait | okm::ulf_no_errno | okm::ulf_wake_all,
reinterpret_cast<okm_long>(&g_stop_word), 0);
}

// The structure this kernel's `sigaction' takes. `sa_tramp' is the second field
// and is the whole reason this is spelled out rather than borrowed.
struct macos_sigaction {
void (*handler)(int);
void (*tramp)(void*, int, int, void*, void*);
unsigned int mask;
int flags;
};

// ⚠️ THE RESULT IS EXAMINED, AND THE FUNCTION EXISTS TO RETURN IT. An
// installation that failed would leave a word that can never change, and
// answering the caller with one is `nothing here reports success having done
// nothing' in its exact form: the program would ask whether its end had been
// requested, be told no, and go on being told no after it had been.
bool arm_one(int signo) {
macos_sigaction act{};
act.handler = &stop_handler;
act.tramp = reinterpret_cast<void (*)(void*, int, int, void*, void*)>(&okm_sigtramp);
return !okm::failed(okm::sys(nr_sigaction, signo,
reinterpret_cast<okm_long>(&act), 0));
}

#endif // __aarch64__

} // namespace

const kal_u32* kal_process_stop_requested(void) {
#if defined(__aarch64__)
// ⚠️ THREE STATES AND NOT TWO: not yet tried, armed, refused. A second
// caller must be told what the first found rather than arming again --- and
// must not be told `not yet tried' while the first is still inside the
// installation.
int state = __atomic_load_n(&g_stop_armed, __ATOMIC_ACQUIRE);
if (state == 0) {
// SIGTERM is the one `kal_process_terminate' sends here; SIGINT is what
// an interactive stream delivers. Both are requests to end, which is the
// whole of what this word reports.
const bool ok = arm_one(15) && arm_one(2);
state = ok ? 1 : -1;
__atomic_store_n(&g_stop_armed, state, __ATOMIC_RELEASE);
}
return state == 1 ? &g_stop_word : nullptr;
#else
// ⚠️ DECLINED ON THE OTHER ARCHITECTURE, AND NOT BECAUSE IT CANNOT BE
// WRITTEN. The trampoline above has an x86_64 counterpart of the same
// length. What it does not have is a way to be RUN: the build tool has no
// release for x86_64 on this system, so continuous integration compiles the
// sources there and executes nothing --- and a trampoline that has never
// been entered is the one thing this operation must not ship. Clause 6.2
// makes the absence a fact a caller reads, and `kal_process_props' below
// does not claim the position.
return 0;
#endif
}

// This program itself joins or forms a unit --- what `kal_spawn.job' cannot say,
// because that places a program the caller STARTS and a copy wishing to lead a
Expand Down Expand Up @@ -309,10 +423,26 @@ void kal_process_close(kal_process) { }
// Starting a program whose lifetime is bound to this one's. Version 0.10.
//

// ⚠️ EVERY POSITION THE SPECIFICATION HAS ASSIGNED IS ACCOUNTED FOR HERE, either
// by being claimed or by being deliberately absent. BOUND_LIFETIME is absent
// because `kal_process_spawn' above refuses every flag; STOP_REQUESTED is
// claimed only where the trampoline it needs has been entered by a running
// program, which is the architecture continuous integration executes.
kal_uintptr kal_process_props(void) { return
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
| KAL_PROCESS_PROP_EXIT_STATUS
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR
| KAL_PROCESS_PROP_JOB; }
| KAL_PROCESS_PROP_JOB
#if defined(__aarch64__)
// ⚠️ AND IT AGREES WITH `kal_process_stop_requested', WHICH IS A REQUIREMENT
// AND NOT A COURTESY: the header defines null there as the absence this
// position reports, so the two cannot disagree. It is read and never armed
// --- asking what an implementation can do must not install a disposition ---
// so this claims the position until an installation has actually been refused,
// and stops claiming it afterwards.
| (__atomic_load_n(&g_stop_armed, __ATOMIC_ACQUIRE) == -1
? 0u : KAL_PROCESS_PROP_STOP_REQUESTED)
#endif
; }

}
104 changes: 104 additions & 0 deletions tests/conformance_process_task.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Conformance: openkal.process and openkal.task.
import openkal.process;
import openkal.macros;
import openkal.task;
import openkal.fs;
import openkal.stream;
Expand Down Expand Up @@ -221,6 +222,109 @@ int main() {
kal_task_yield();
check(kal_task_current() != 0, "the calling context has an identity");

// --- being told that an end has been requested ---------------------------
//
// ⚠️⚠️ THIS RAISES THE SIGNAL. Compiling a disposition and never delivering
// one proves nothing here: what this operation needs on this kernel is a
// TRAMPOLINE, `sa_tramp' in the structure the raw `sigaction' takes, and a
// wrong one is not a wrong answer --- it is a program that dies inside the
// handler, at an address nobody can attribute. openkal-macos declined to
// claim KAL_PROCESS_PROP_STOP_REQUESTED until this check existed, and this
// comment is why the order was that way round.
//
// ⭐ The signal is raised by a shell, which is how a program reaches its own
// kernel here without leaving openkal's vocabulary: `kal_process_job_enter'
// with a zero unit reports the identifier this program runs under, and that
// is the identifier the shell needs.
{
const kal_u32* word = kal_process_stop_requested();
if (word == nullptr) {
// Declined, which clause 6.2 permits and KAL_PROCESS_PROP_STOP_REQUESTED
// states. Nothing below applies --- and the log says which of the two
// this run was, for the reason given at the observation further down.
say(" stop-request: declined, and the position is not claimed\n");
check((kal_process_props() & kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M) == 0,
"an implementation that answers no word does not claim the position");
} else {
check((kal_process_props() & kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M) != 0,
"an implementation that answers a word claims the position");
check(*word == 0, "and nothing has asked this program to end yet");

kal_job unit{};
const int entered = kal_process_job_enter(&unit);
check(entered == kal_ok, "this program's identifier is reported");

if (entered == kal_ok && unit.h != 0) {
// "kill -TERM <pid>" with the number written out. The shell is
// given a moment first so that the wait below is entered rather
// than raced past --- the check does not depend on it, because a
// word already set makes the wait return at once.
char script[64];
kal_uintptr n = 0;
const char lead[] = "sleep 0.3; kill -TERM ";
for (kal_uintptr i = 0; i < sizeof(lead) - 1; ++i) script[n++] = lead[i];
char digits[24]; int d = 24;
kal_uintptr v = unit.h;
if (v == 0) digits[--d] = '0';
while (v > 0) { digits[--d] = static_cast<char>('0' + v % 10); v /= 10; }
while (d < 24) script[n++] = digits[d++];
script[n] = 0;

// Located by asking, as above --- the lambda that does it is
// scoped to the block above, and duplicating four lines is
// better than widening something for one caller.
const char* sh_paths[] = { "bin/sh", "usr/bin/sh" };
const kal_uintptr sh_lens[] = { 6, 10 };
int sh = -1;
for (int i = 0; i < 2 && sh < 0; ++i) {
kal_node_info info{}; info.self_size = sizeof info;
if (kal_fs_info(slash, sh_paths[i], sh_lens[i], 0,
kal::fs::field::kind, &info) != kal_ok) continue;
if (info.kind != kal_node_absent) sh = i;
}
check(sh >= 0, "a shell is found to raise the signal");

kal_process k{};
const kal_spawn how{ slash, slash, nullptr, nullptr, 0, 0 };
const char* kargv[] = { "sh", "-c", script };
const kal_uintptr klens[] = { 2, 2, n };
int krc = kal_err_invalid;
if (sh >= 0)
krc = kal_process_spawn(&how, sh_paths[sh], sh_lens[sh],
kargv, klens, 3,
nullptr, nullptr, 0, nullptr, &k);
check(krc == kal_ok, "the program that raises the signal starts");

if (krc == kal_ok) {
// Up to five seconds, in bounded waits upon the word itself
// --- which is the use the word was specified for.
for (int i = 0; i < 50 && *word == 0; ++i)
kal_task_wait(word, 0u, 100ull * 1000 * 1000);

check(*word != 0, "the program is told that its end was requested");

// ⭐ AND IT IS STILL RUNNING, which is the half a compiled
// disposition cannot show. Reaching this line is the proof:
// a program that died in the handler never gets here.
//
// ⚠️⚠️ SAID ALOUD, AND EVERY OTHER CHECK HERE IS SILENT WHEN
// IT HOLDS. That convention cannot serve this one. Each
// observation above is skipped rather than failed when its
// precondition is absent --- no root, no word, no shell ---
// so a green run is consistent BOTH with a trampoline that
// was entered and returned and with a block that never ran.
// Those are the two outcomes this check exists to tell
// apart, and only a line in the log tells them apart.
if (*word != 0) say(" stop-request: told, and still running\n");

int status = -1, terminated = -1;
kal_process_wait(k, &status, &terminated);
kal_process_close(k);
}
}
}
}

const char ok[] = "openkal-macos: process and task conformance\n";
kal::write(kal::out(), ok, sizeof(ok) - 1);
return failures == 0 ? 0 : 1;
Expand Down
Loading