diff --git a/mcpp.toml b/mcpp.toml index d992781..b59416d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" @@ -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 diff --git a/src/process.cpp b/src/process.cpp index 02041c4..6c2c573 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -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(&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(&okm_sigtramp); + return !okm::failed(okm::sys(nr_sigaction, signo, + reinterpret_cast(&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 @@ -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 + ; } } diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index 9517415..45eb2c2 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -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; @@ -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 " 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('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;