Skip to content

Commit 6546f16

Browse files
authored
0.9.0 — the word this kernel could not set without a trampoline of its own (#18)
* 0.9.0 — the word this kernel could not set without a trampoline of its own `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. * 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. * 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.
1 parent 0592e26 commit 6546f16

3 files changed

Lines changed: 238 additions & 4 deletions

File tree

mcpp.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-macos"
4-
version = "0.8.0"
4+
version = "0.9.0"
55
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."
66
license = "Apache-2.0"
77

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

2020
[dependencies]
21-
openkal = "0.11.0"
21+
openkal = "0.12.0"
2222

2323
[build]
2424
# The flags are attached to this package's own sources rather than to the whole

src/process.cpp

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,121 @@ int kal_process_terminate(kal_process h) {
270270
// ⇒ Null, and KAL_PROCESS_PROP_STOP_REQUESTED unclaimed, so a caller that asks
271271
// first is told. The other implementation answers it; this one will when it can
272272
// be exercised here.
273-
const kal_u32* kal_process_stop_requested(void) { return 0; }
273+
// ⭐⭐ A WORD THIS PROGRAM'S ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED IT TO END.
274+
//
275+
// ⚠️⚠️ THE TRAMPOLINE IS THIS IMPLEMENTATION'S OWN, WHICH IS WHY THIS ARRIVED A
276+
// VERSION LATE. The raw `sigaction' of this kernel takes a structure whose
277+
// SECOND field is `sa_tramp': the kernel enters that address, not the handler,
278+
// and the handler is passed to it as an argument. A C library ordinarily
279+
// supplies it (`_sigtramp' in libsystem) and there is no C library beneath this
280+
// implementation. A structure installed with a null or wrong `sa_tramp' does not
281+
// fail at installation --- it fails on DELIVERY, inside the handler, at an
282+
// address that belongs to nobody, which is the least attributable failure this
283+
// implementation could ship. So the order was: a conformance check that raises
284+
// the signal first, this second.
285+
//
286+
// ⚠️ ARMED ON THE FIRST ENQUIRY AND NOT AT STARTUP, exactly as openkal-linux
287+
// argues: a program that never asks keeps the default action, and adding this
288+
// operation therefore changes nothing for anyone who does not use it.
289+
namespace {
290+
291+
kal_u32 g_stop_word = 0;
292+
int g_stop_armed = 0;
293+
294+
#if defined(__aarch64__)
295+
296+
constexpr okm_long nr_sigaction = 46;
297+
298+
// What the kernel enters. Its arguments are (handler, infostyle, sig, siginfo,
299+
// ucontext) in x0..x4; it calls the handler with the last three and then asks
300+
// the kernel to restore the interrupted context.
301+
//
302+
// ⚠️ x19 AND x20 ARE USED WITHOUT BEING SAVED, and that is correct here rather
303+
// than sloppy: this function does not return to its caller. `sigreturn' restores
304+
// the whole of the interrupted context, callee-saved registers included, so the
305+
// values these two held belong to a frame the kernel is about to reinstate.
306+
extern "C" void okm_sigtramp(void);
307+
asm(".globl _okm_sigtramp\n"
308+
".p2align 2\n"
309+
"_okm_sigtramp:\n"
310+
" mov x19, x1\n" // infostyle
311+
" mov x20, x4\n" // ucontext
312+
" mov x8, x0\n" // handler
313+
" mov x0, x2\n" // sig
314+
" mov x1, x3\n" // siginfo
315+
" mov x2, x20\n" // ucontext
316+
" blr x8\n"
317+
" mov x0, x20\n" // ucontext
318+
" mov x1, x19\n" // infostyle
319+
" mov x16, #184\n" // SYS_sigreturn
320+
" svc #0x80\n"
321+
" brk #1\n"); // sigreturn does not come back
322+
323+
void stop_handler(int) {
324+
__atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE);
325+
// Woken through the same operation `kal_task_wake' performs, issued as the
326+
// raw call because a handler may not enter code that takes a lock. Waking
327+
// ALL of them: any number of contexts may be waiting upon this one word, and
328+
// the handler has no way to learn how many.
329+
okm::sys(okm::nr_ulock_wake,
330+
okm::ul_compare_and_wait | okm::ulf_no_errno | okm::ulf_wake_all,
331+
reinterpret_cast<okm_long>(&g_stop_word), 0);
332+
}
333+
334+
// The structure this kernel's `sigaction' takes. `sa_tramp' is the second field
335+
// and is the whole reason this is spelled out rather than borrowed.
336+
struct macos_sigaction {
337+
void (*handler)(int);
338+
void (*tramp)(void*, int, int, void*, void*);
339+
unsigned int mask;
340+
int flags;
341+
};
342+
343+
// ⚠️ THE RESULT IS EXAMINED, AND THE FUNCTION EXISTS TO RETURN IT. An
344+
// installation that failed would leave a word that can never change, and
345+
// answering the caller with one is `nothing here reports success having done
346+
// nothing' in its exact form: the program would ask whether its end had been
347+
// requested, be told no, and go on being told no after it had been.
348+
bool arm_one(int signo) {
349+
macos_sigaction act{};
350+
act.handler = &stop_handler;
351+
act.tramp = reinterpret_cast<void (*)(void*, int, int, void*, void*)>(&okm_sigtramp);
352+
return !okm::failed(okm::sys(nr_sigaction, signo,
353+
reinterpret_cast<okm_long>(&act), 0));
354+
}
355+
356+
#endif // __aarch64__
357+
358+
} // namespace
359+
360+
const kal_u32* kal_process_stop_requested(void) {
361+
#if defined(__aarch64__)
362+
// ⚠️ THREE STATES AND NOT TWO: not yet tried, armed, refused. A second
363+
// caller must be told what the first found rather than arming again --- and
364+
// must not be told `not yet tried' while the first is still inside the
365+
// installation.
366+
int state = __atomic_load_n(&g_stop_armed, __ATOMIC_ACQUIRE);
367+
if (state == 0) {
368+
// SIGTERM is the one `kal_process_terminate' sends here; SIGINT is what
369+
// an interactive stream delivers. Both are requests to end, which is the
370+
// whole of what this word reports.
371+
const bool ok = arm_one(15) && arm_one(2);
372+
state = ok ? 1 : -1;
373+
__atomic_store_n(&g_stop_armed, state, __ATOMIC_RELEASE);
374+
}
375+
return state == 1 ? &g_stop_word : nullptr;
376+
#else
377+
// ⚠️ DECLINED ON THE OTHER ARCHITECTURE, AND NOT BECAUSE IT CANNOT BE
378+
// WRITTEN. The trampoline above has an x86_64 counterpart of the same
379+
// length. What it does not have is a way to be RUN: the build tool has no
380+
// release for x86_64 on this system, so continuous integration compiles the
381+
// sources there and executes nothing --- and a trampoline that has never
382+
// been entered is the one thing this operation must not ship. Clause 6.2
383+
// makes the absence a fact a caller reads, and `kal_process_props' below
384+
// does not claim the position.
385+
return 0;
386+
#endif
387+
}
274388

275389
// This program itself joins or forms a unit --- what `kal_spawn.job' cannot say,
276390
// 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) { }
309423
// Starting a program whose lifetime is bound to this one's. Version 0.10.
310424
//
311425

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

318448
}

tests/conformance_process_task.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Conformance: openkal.process and openkal.task.
22
import openkal.process;
3+
import openkal.macros;
34
import openkal.task;
45
import openkal.fs;
56
import openkal.stream;
@@ -221,6 +222,109 @@ int main() {
221222
kal_task_yield();
222223
check(kal_task_current() != 0, "the calling context has an identity");
223224

225+
// --- being told that an end has been requested ---------------------------
226+
//
227+
// ⚠️⚠️ THIS RAISES THE SIGNAL. Compiling a disposition and never delivering
228+
// one proves nothing here: what this operation needs on this kernel is a
229+
// TRAMPOLINE, `sa_tramp' in the structure the raw `sigaction' takes, and a
230+
// wrong one is not a wrong answer --- it is a program that dies inside the
231+
// handler, at an address nobody can attribute. openkal-macos declined to
232+
// claim KAL_PROCESS_PROP_STOP_REQUESTED until this check existed, and this
233+
// comment is why the order was that way round.
234+
//
235+
// ⭐ The signal is raised by a shell, which is how a program reaches its own
236+
// kernel here without leaving openkal's vocabulary: `kal_process_job_enter'
237+
// with a zero unit reports the identifier this program runs under, and that
238+
// is the identifier the shell needs.
239+
{
240+
const kal_u32* word = kal_process_stop_requested();
241+
if (word == nullptr) {
242+
// Declined, which clause 6.2 permits and KAL_PROCESS_PROP_STOP_REQUESTED
243+
// states. Nothing below applies --- and the log says which of the two
244+
// this run was, for the reason given at the observation further down.
245+
say(" stop-request: declined, and the position is not claimed\n");
246+
check((kal_process_props() & kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M) == 0,
247+
"an implementation that answers no word does not claim the position");
248+
} else {
249+
check((kal_process_props() & kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M) != 0,
250+
"an implementation that answers a word claims the position");
251+
check(*word == 0, "and nothing has asked this program to end yet");
252+
253+
kal_job unit{};
254+
const int entered = kal_process_job_enter(&unit);
255+
check(entered == kal_ok, "this program's identifier is reported");
256+
257+
if (entered == kal_ok && unit.h != 0) {
258+
// "kill -TERM <pid>" with the number written out. The shell is
259+
// given a moment first so that the wait below is entered rather
260+
// than raced past --- the check does not depend on it, because a
261+
// word already set makes the wait return at once.
262+
char script[64];
263+
kal_uintptr n = 0;
264+
const char lead[] = "sleep 0.3; kill -TERM ";
265+
for (kal_uintptr i = 0; i < sizeof(lead) - 1; ++i) script[n++] = lead[i];
266+
char digits[24]; int d = 24;
267+
kal_uintptr v = unit.h;
268+
if (v == 0) digits[--d] = '0';
269+
while (v > 0) { digits[--d] = static_cast<char>('0' + v % 10); v /= 10; }
270+
while (d < 24) script[n++] = digits[d++];
271+
script[n] = 0;
272+
273+
// Located by asking, as above --- the lambda that does it is
274+
// scoped to the block above, and duplicating four lines is
275+
// better than widening something for one caller.
276+
const char* sh_paths[] = { "bin/sh", "usr/bin/sh" };
277+
const kal_uintptr sh_lens[] = { 6, 10 };
278+
int sh = -1;
279+
for (int i = 0; i < 2 && sh < 0; ++i) {
280+
kal_node_info info{}; info.self_size = sizeof info;
281+
if (kal_fs_info(slash, sh_paths[i], sh_lens[i], 0,
282+
kal::fs::field::kind, &info) != kal_ok) continue;
283+
if (info.kind != kal_node_absent) sh = i;
284+
}
285+
check(sh >= 0, "a shell is found to raise the signal");
286+
287+
kal_process k{};
288+
const kal_spawn how{ slash, slash, nullptr, nullptr, 0, 0 };
289+
const char* kargv[] = { "sh", "-c", script };
290+
const kal_uintptr klens[] = { 2, 2, n };
291+
int krc = kal_err_invalid;
292+
if (sh >= 0)
293+
krc = kal_process_spawn(&how, sh_paths[sh], sh_lens[sh],
294+
kargv, klens, 3,
295+
nullptr, nullptr, 0, nullptr, &k);
296+
check(krc == kal_ok, "the program that raises the signal starts");
297+
298+
if (krc == kal_ok) {
299+
// Up to five seconds, in bounded waits upon the word itself
300+
// --- which is the use the word was specified for.
301+
for (int i = 0; i < 50 && *word == 0; ++i)
302+
kal_task_wait(word, 0u, 100ull * 1000 * 1000);
303+
304+
check(*word != 0, "the program is told that its end was requested");
305+
306+
// ⭐ AND IT IS STILL RUNNING, which is the half a compiled
307+
// disposition cannot show. Reaching this line is the proof:
308+
// a program that died in the handler never gets here.
309+
//
310+
// ⚠️⚠️ SAID ALOUD, AND EVERY OTHER CHECK HERE IS SILENT WHEN
311+
// IT HOLDS. That convention cannot serve this one. Each
312+
// observation above is skipped rather than failed when its
313+
// precondition is absent --- no root, no word, no shell ---
314+
// so a green run is consistent BOTH with a trampoline that
315+
// was entered and returned and with a block that never ran.
316+
// Those are the two outcomes this check exists to tell
317+
// apart, and only a line in the log tells them apart.
318+
if (*word != 0) say(" stop-request: told, and still running\n");
319+
320+
int status = -1, terminated = -1;
321+
kal_process_wait(k, &status, &terminated);
322+
kal_process_close(k);
323+
}
324+
}
325+
}
326+
}
327+
224328
const char ok[] = "openkal-macos: process and task conformance\n";
225329
kal::write(kal::out(), ok, sizeof(ok) - 1);
226330
return failures == 0 ? 0 : 1;

0 commit comments

Comments
 (0)