From 4a34f241119368e08f546e5e711546fd4eb4dc69 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 08:09:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?0.9.0=20=E2=80=94=20the=20word=20this=20ker?= =?UTF-8?q?nel=20could=20not=20set=20without=20a=20trampoline=20of=20its?= =?UTF-8?q?=20own?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kal_process_stop_requested` answered null here while two other implementations answered a word. The reason was never the specification: the raw `sigaction` of this kernel takes a structure whose SECOND field is `sa_tramp`, the kernel enters THAT address rather than the handler, and the C library that ordinarily supplies it (`_sigtramp`) is not beneath this implementation. ⚠️ A wrong trampoline is not a wrong answer — it is a program that dies inside the handler at an address belonging to nobody. So the order was: **the check that raises the signal first, the trampoline second.** The check installs the disposition, has a shell raise SIGTERM at this program, waits on the word through `kal_task_wait`, and then asserts the program is STILL RUNNING — which reaching the line proves and a compiled disposition cannot show. ⭐ arm64 only, and that is the whole of it rather than half: the CI matrix is `macos-14` alone because the build tool has no x86_64 release for this system, so a trampoline there could be compiled and never entered. Clause 6.2 makes the absence a fact a caller reads, and `kal_process_props` claims the position only where it has been run. ⚠️ The test is this ecosystem's first consumer of `openkal.macros`, and writing it is how the module's own gap was found — see openkal. --- mcpp.toml | 4 +- src/process.cpp | 112 ++++++++++++++++++++++++++++- tests/conformance_process_task.cpp | 93 ++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 4 deletions(-) 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..59dbc50 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -270,7 +270,106 @@ 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; +}; + +void arm_one(int signo) { + macos_sigaction act{}; + act.handler = &stop_handler; + act.tramp = reinterpret_cast(&okm_sigtramp); + okm::sys(nr_sigaction, signo, reinterpret_cast(&act), 0); +} + +#endif // __aarch64__ + +} // namespace + +const kal_u32* kal_process_stop_requested(void) { +#if defined(__aarch64__) + if (!__atomic_exchange_n(&g_stop_armed, 1, __ATOMIC_ACQ_REL)) { + arm_one(15); // SIGTERM + arm_one(2); // SIGINT + } + return &g_stop_word; +#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 +408,19 @@ 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__) + | KAL_PROCESS_PROP_STOP_REQUESTED +#endif + ; } } diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index 9517415..7aabafb 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,98 @@ 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. + 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. + check(true, "and it is still running, having survived delivery"); + + 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; From 256ac2896c2f614fa2a790d1edc4e1c3564a7288 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 08:13:58 +0800 Subject: [PATCH 2/3] Say which branch the stop-request observation took Every check in this file is silent when it holds, and that convention cannot serve this one. Each observation is SKIPPED rather than failed when its precondition is absent --- no root directory, no word, no shell --- so a green run was consistent both with a trampoline that was entered and returned and with a block that never ran at all. Those are exactly the two outcomes the check exists to tell apart. Noticed from the run that first exercised it: the job reported `ok (0.52s)', which is consistent with the 0.3s sleep the check spawns and proves nothing on its own. --- tests/conformance_process_task.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index 7aabafb..45eb2c2 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -240,7 +240,9 @@ int main() { 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. + // 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 { @@ -304,7 +306,16 @@ int main() { // ⭐ 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. - check(true, "and it is still running, having survived delivery"); + // + // ⚠️⚠️ 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); From 1f0e8b9840077b24f8ceb93222f019cb6ef2232c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 08:34:20 +0800 Subject: [PATCH 3/3] Examine whether the disposition was installed Found in self-review, and it is the shape this ecosystem exists to exclude: the installation's result was discarded, so a failed one would have left a word that can never change while the caller was handed it anyway. The program would ask whether its end had been requested, be told no, and go on being told no after it had been. `kal_process_props' now agrees, because the header defines null there as the absence that position reports and the two cannot disagree. It reads the state and never arms --- asking what an implementation can do must not install a disposition. --- src/process.cpp | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/process.cpp b/src/process.cpp index 59dbc50..6c2c573 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -340,11 +340,17 @@ struct macos_sigaction { int flags; }; -void arm_one(int signo) { +// ⚠️ 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); - okm::sys(nr_sigaction, signo, reinterpret_cast(&act), 0); + return !okm::failed(okm::sys(nr_sigaction, signo, + reinterpret_cast(&act), 0)); } #endif // __aarch64__ @@ -353,11 +359,20 @@ void arm_one(int signo) { const kal_u32* kal_process_stop_requested(void) { #if defined(__aarch64__) - if (!__atomic_exchange_n(&g_stop_armed, 1, __ATOMIC_ACQ_REL)) { - arm_one(15); // SIGTERM - arm_one(2); // SIGINT + // ⚠️ 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 &g_stop_word; + 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 @@ -419,7 +434,14 @@ kal_uintptr kal_process_props(void) { return | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR | KAL_PROCESS_PROP_JOB #if defined(__aarch64__) - | KAL_PROCESS_PROP_STOP_REQUESTED + // ⚠️ 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 ; }