Skip to content

Commit 4a34f24

Browse files
committed
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.
1 parent 0592e26 commit 4a34f24

3 files changed

Lines changed: 205 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: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,106 @@ 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+
void arm_one(int signo) {
344+
macos_sigaction act{};
345+
act.handler = &stop_handler;
346+
act.tramp = reinterpret_cast<void (*)(void*, int, int, void*, void*)>(&okm_sigtramp);
347+
okm::sys(nr_sigaction, signo, reinterpret_cast<okm_long>(&act), 0);
348+
}
349+
350+
#endif // __aarch64__
351+
352+
} // namespace
353+
354+
const kal_u32* kal_process_stop_requested(void) {
355+
#if defined(__aarch64__)
356+
if (!__atomic_exchange_n(&g_stop_armed, 1, __ATOMIC_ACQ_REL)) {
357+
arm_one(15); // SIGTERM
358+
arm_one(2); // SIGINT
359+
}
360+
return &g_stop_word;
361+
#else
362+
// ⚠️ DECLINED ON THE OTHER ARCHITECTURE, AND NOT BECAUSE IT CANNOT BE
363+
// WRITTEN. The trampoline above has an x86_64 counterpart of the same
364+
// length. What it does not have is a way to be RUN: the build tool has no
365+
// release for x86_64 on this system, so continuous integration compiles the
366+
// sources there and executes nothing --- and a trampoline that has never
367+
// been entered is the one thing this operation must not ship. Clause 6.2
368+
// makes the absence a fact a caller reads, and `kal_process_props' below
369+
// does not claim the position.
370+
return 0;
371+
#endif
372+
}
274373

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

411+
// ⚠️ EVERY POSITION THE SPECIFICATION HAS ASSIGNED IS ACCOUNTED FOR HERE, either
412+
// by being claimed or by being deliberately absent. BOUND_LIFETIME is absent
413+
// because `kal_process_spawn' above refuses every flag; STOP_REQUESTED is
414+
// claimed only where the trampoline it needs has been entered by a running
415+
// program, which is the architecture continuous integration executes.
312416
kal_uintptr kal_process_props(void) { return
313417
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
314418
| KAL_PROCESS_PROP_EXIT_STATUS
315419
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR
316-
| KAL_PROCESS_PROP_JOB; }
420+
| KAL_PROCESS_PROP_JOB
421+
#if defined(__aarch64__)
422+
| KAL_PROCESS_PROP_STOP_REQUESTED
423+
#endif
424+
; }
317425

318426
}

tests/conformance_process_task.cpp

Lines changed: 93 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,98 @@ 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.
244+
check((kal_process_props() & kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M) == 0,
245+
"an implementation that answers no word does not claim the position");
246+
} else {
247+
check((kal_process_props() & kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M) != 0,
248+
"an implementation that answers a word claims the position");
249+
check(*word == 0, "and nothing has asked this program to end yet");
250+
251+
kal_job unit{};
252+
const int entered = kal_process_job_enter(&unit);
253+
check(entered == kal_ok, "this program's identifier is reported");
254+
255+
if (entered == kal_ok && unit.h != 0) {
256+
// "kill -TERM <pid>" with the number written out. The shell is
257+
// given a moment first so that the wait below is entered rather
258+
// than raced past --- the check does not depend on it, because a
259+
// word already set makes the wait return at once.
260+
char script[64];
261+
kal_uintptr n = 0;
262+
const char lead[] = "sleep 0.3; kill -TERM ";
263+
for (kal_uintptr i = 0; i < sizeof(lead) - 1; ++i) script[n++] = lead[i];
264+
char digits[24]; int d = 24;
265+
kal_uintptr v = unit.h;
266+
if (v == 0) digits[--d] = '0';
267+
while (v > 0) { digits[--d] = static_cast<char>('0' + v % 10); v /= 10; }
268+
while (d < 24) script[n++] = digits[d++];
269+
script[n] = 0;
270+
271+
// Located by asking, as above --- the lambda that does it is
272+
// scoped to the block above, and duplicating four lines is
273+
// better than widening something for one caller.
274+
const char* sh_paths[] = { "bin/sh", "usr/bin/sh" };
275+
const kal_uintptr sh_lens[] = { 6, 10 };
276+
int sh = -1;
277+
for (int i = 0; i < 2 && sh < 0; ++i) {
278+
kal_node_info info{}; info.self_size = sizeof info;
279+
if (kal_fs_info(slash, sh_paths[i], sh_lens[i], 0,
280+
kal::fs::field::kind, &info) != kal_ok) continue;
281+
if (info.kind != kal_node_absent) sh = i;
282+
}
283+
check(sh >= 0, "a shell is found to raise the signal");
284+
285+
kal_process k{};
286+
const kal_spawn how{ slash, slash, nullptr, nullptr, 0, 0 };
287+
const char* kargv[] = { "sh", "-c", script };
288+
const kal_uintptr klens[] = { 2, 2, n };
289+
int krc = kal_err_invalid;
290+
if (sh >= 0)
291+
krc = kal_process_spawn(&how, sh_paths[sh], sh_lens[sh],
292+
kargv, klens, 3,
293+
nullptr, nullptr, 0, nullptr, &k);
294+
check(krc == kal_ok, "the program that raises the signal starts");
295+
296+
if (krc == kal_ok) {
297+
// Up to five seconds, in bounded waits upon the word itself
298+
// --- which is the use the word was specified for.
299+
for (int i = 0; i < 50 && *word == 0; ++i)
300+
kal_task_wait(word, 0u, 100ull * 1000 * 1000);
301+
302+
check(*word != 0, "the program is told that its end was requested");
303+
304+
// ⭐ AND IT IS STILL RUNNING, which is the half a compiled
305+
// disposition cannot show. Reaching this line is the proof:
306+
// a program that died in the handler never gets here.
307+
check(true, "and it is still running, having survived delivery");
308+
309+
int status = -1, terminated = -1;
310+
kal_process_wait(k, &status, &terminated);
311+
kal_process_close(k);
312+
}
313+
}
314+
}
315+
}
316+
224317
const char ok[] = "openkal-macos: process and task conformance\n";
225318
kal::write(kal::out(), ok, sizeof(ok) - 1);
226319
return failures == 0 ? 0 : 1;

0 commit comments

Comments
 (0)