From 8d09318f83e452c62afd906894a958001ead2473 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 21:17:08 +0800 Subject: [PATCH 1/5] 0.8.0 --- one spawn, and the divergence this implementation had been hiding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THIS IS WHERE THE MISSING MODIFIER WAS ALREADY VISIBLE, and it is recorded rather than quietly fixed. This kernel has no `execveat', so a program named relative to a directory has always been started by ENTERING that directory first. A started program's working directory was therefore `base' here --- and on the other kernel it was whatever that implementation happened to be in. ⭐ Same openkal calls, two different observable answers, and NEITHER WAS WRONG, because the specification said nothing about it. That is the shape clause 11 entry 13 already records for a different operation: a divergence caused by a missing declaration is a defect of the specification. ⇒ 0.11 gives the caller a second directory and both implementations now enter the one the caller named. The program's name is made absolute first, through the `F_GETPATH' this repository already uses in src/fs.cpp for the same reason --- with no `execveat' one `fchdir' cannot serve both meanings. KAL_SPAWN_OWN_JOB is claimed and implemented (`setpgid' in the duplicate); kal_process_terminate reaches the group when the started program formed one, and recovers that fact with `getpgid(pid) == pid' rather than storing it. KAL_SPAWN_BOUND_LIFETIME stays refused, for the reason 0.10 recorded: this system offers a watch, a watch needs a live context to notice, and a caller killed outright notices nothing. --- mcpp.toml | 4 +- src/process.cpp | 215 +++++++++++++++++++++--------------------------- 2 files changed, 97 insertions(+), 122 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 64bb9af..d992781 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.7.0" +version = "0.8.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.10.0" +openkal = "0.11.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 54defc1..8ec69c3 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -56,35 +56,95 @@ struct vector { }; constexpr okm_long nr_fchdir = 13; +// openkal 0.11: a started program that forms a job of its own. +constexpr okm_long nr_setpgid = 82; +constexpr okm_long nr_getpgid = 151; } // namespace extern "C" { -int kal_process_spawn(kal_dir base, +// Starting a program. One function since openkal 0.11, where three declarations +// became one and their modifiers became positions in `kal_spawn'. +// +// ⚠️⚠️ AND THIS IMPLEMENTATION IS WHERE THE MISSING MODIFIER WAS ALREADY VISIBLE, +// which is worth recording rather than quietly fixing. +// +// This kernel has no `execveat', so a program named relative to a directory has +// always been started by entering that directory first --- the `fchdir(base)' +// below used to be the whole story. So a started program's working directory WAS +// `base' here, and on the other kernel it was whatever that implementation +// happened to be in. ⭐ Same openkal calls, two different observable answers, +// and neither was wrong because the specification said nothing. +// +// ⇒ 0.11 gives the caller a second directory, and both implementations now enter +// the one the caller named. The divergence is gone because the thing that caused +// it --- a property nobody had declared --- is declared. +int kal_process_spawn(const kal_spawn* how, const char* path, kal_uintptr path_len, const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, const kal_spawn_streams* streams, kal_process* out) { - const int b = okm::unpack(base.h); - if (b < 0 || out == nullptr) return kal_err_invalid; + if (how == nullptr || out == nullptr) return kal_err_invalid; + + const int b = okm::unpack(how->base.h); + const int w = okm::unpack(how->work.h); + if (b < 0 || w < 0) return kal_err_invalid; if (!okm::acceptable(path, path_len)) return kal_err_invalid; + if (how->grant_count > 0 && how->grants == nullptr) return kal_err_invalid; + + // ⚠️ A LIFETIME THIS KERNEL CANNOT BIND IS REFUSED BEFORE ANYTHING STARTS. + // The reason is unchanged from 0.10 and is stated at kal_process_props: the + // binding must hold however the caller ends, including when it is killed + // outright, and what this system offers instead is a WATCH, which needs a + // live context to notice. Composing it would move the defect from "refused" + // to "works except when it matters". + constexpr kal_uintptr can = KAL_SPAWN_OWN_JOB; + if (how->flags & ~can) return kal_err_not_supported; + okm::terminated p(path, path_len); if (!p.ok) return kal_err_invalid; - // The vector is passed unaltered. Clause 7.6: argv[0] is the name the - // started program observes as its own, and it is the caller's to choose --- - // the started program reads it through kal_env_arg(0), so a caller that did - // not supply it could not predict what the program would read. vector args, envs; if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; + constexpr kal_uintptr max_grants = 16; + if (how->grant_count > max_grants) return kal_err_invalid; + int granted[max_grants]; + for (kal_uintptr i = 0; i < how->grant_count; ++i) { + granted[i] = okm::unpack(how->grants[i].dir.h); + if (granted[i] < 0) return kal_err_invalid; + } + + // ⭐ THE PROGRAM'S NAME IS MADE ABSOLUTE BEFORE THE DIRECTORY MOVES, because + // with no `execveat' the two things `base' and `work' now mean cannot both be + // served by one `fchdir'. `F_GETPATH' answers the path of an open directory, + // which src/fs.cpp already relies on for the same reason: this kernel has no + // call that reports a working directory, so a path is obtained from the + // descriptor that names it. + char whole[1024]; + kal_uintptr n = 0; + if (p.buf[0] == '/') { + while (p.buf[n] && n < sizeof whole - 1) { whole[n] = p.buf[n]; ++n; } + } else { + const okm_long r = okm::sys(okm::nr_fcntl, b, okm::f_getpath, + reinterpret_cast(whole)); + if (okm::failed(r)) return okm::translate(r); + while (whole[n] && n < sizeof whole - 1) ++n; + if (n && whole[n - 1] != '/' && n < sizeof whole - 1) whole[n++] = '/'; + for (kal_uintptr i = 0; p.buf[i] && n < sizeof whole - 1; ++i) whole[n++] = p.buf[i]; + } + if (n >= sizeof whole - 1) return kal_err_invalid; + whole[n] = '\0'; + const okm_long in = streams ? static_cast(streams->in.h) : 0; const okm_long ou = streams ? static_cast(streams->out.h) : 0; const okm_long er = streams ? static_cast(streams->err.h) : 0; + const bool job = (how->flags & KAL_SPAWN_OWN_JOB) != 0; + bool is_duplicate = false; const okm_long child = okm::duplicate(is_duplicate); if (okm::failed(child)) return okm::translate(child); @@ -97,8 +157,19 @@ int kal_process_spawn(kal_dir base, if (in != 0) okm::sys(okm::nr_dup2, in, 0); if (ou != 0) okm::sys(okm::nr_dup2, ou, 1); if (er != 0) okm::sys(okm::nr_dup2, er, 2); - okm::sys(nr_fchdir, b); - okm::sys(okm::nr_execve, reinterpret_cast(p.buf), + + for (kal_uintptr i = 0; i < how->grant_count; ++i) { + const okm_long want = static_cast(3 + i); + if (granted[i] != want) okm::sys(okm::nr_dup2, granted[i], want); + } + + // The directory the program RUNS in --- `whole' already carries where it + // is named from, so this no longer has to serve both. + okm::sys(nr_fchdir, w); + + if (job) okm::sys(nr_setpgid, 0, 0); + + okm::sys(okm::nr_execve, reinterpret_cast(whole), reinterpret_cast(args.slots), reinterpret_cast(envs.slots)); for (;;) okm::sys(okm::nr_exit, 127); @@ -149,97 +220,22 @@ void kal_process_channel_close(kal_stream s) { } // Starting a program that receives exactly the directories named. -// -// The grants are placed as descriptors three and upward, which is where -// kal_fs_preopen reads them back from. The inverse relationship clause 7.11 -// describes is between those two operations, which is why they must agree about -// the numbering rather than each choosing one. -int kal_process_spawn_with(kal_dir base, - const char* path, kal_uintptr path_len, - const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, - const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, - const kal_spawn_streams* streams, - const kal_preopen* grants, kal_uintptr grant_count, - kal_process* out) { - const int b = okm::unpack(base.h); - if (b < 0 || out == nullptr) return kal_err_invalid; - if (!okm::acceptable(path, path_len)) return kal_err_invalid; - if (grant_count > 0 && grants == nullptr) return kal_err_invalid; - okm::terminated p(path, path_len); - if (!p.ok) return kal_err_invalid; - - vector args, envs; - if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; - if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; - - // Resolved before the duplication, because a failure after it would leave a - // child to be reaped and a caller holding an error it cannot act upon. - constexpr kal_uintptr max_grants = 16; - if (grant_count > max_grants) return kal_err_invalid; - int granted[max_grants]; - for (kal_uintptr i = 0; i < grant_count; ++i) { - granted[i] = okm::unpack(grants[i].dir.h); - if (granted[i] < 0) return kal_err_invalid; - } - - const okm_long in = streams ? static_cast(streams->in.h) : 0; - const okm_long ou = streams ? static_cast(streams->out.h) : 0; - const okm_long er = streams ? static_cast(streams->err.h) : 0; - - bool is_duplicate = false; - const okm_long child = okm::duplicate(is_duplicate); - if (okm::failed(child)) return okm::translate(child); - - if (is_duplicate) { - if (in != 0) okm::sys(okm::nr_dup2, in, 0); - if (ou != 0) okm::sys(okm::nr_dup2, ou, 1); - if (er != 0) okm::sys(okm::nr_dup2, er, 2); - - // dup2 onto the same number succeeds and does nothing, unlike dup3, - // which refuses. Either behaviour is right for this loop; only the - // reason differs, and it is stated so that a reader comparing the two - // implementations does not take one of them for an oversight. - for (kal_uintptr i = 0; i < grant_count; ++i) - okm::sys(okm::nr_dup2, granted[i], static_cast(3 + i)); - - okm::sys(nr_fchdir, b); - okm::sys(okm::nr_execve, reinterpret_cast(p.buf), - reinterpret_cast(args.slots), - reinterpret_cast(envs.slots)); - for (;;) okm::sys(okm::nr_exit, 127); - } - - *out = kal_process{ static_cast(child) }; - return kal_ok; -} - -int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) { - if (h.h == 0) return kal_err_invalid; - int st = 0; - for (;;) { - const okm_long r = okm::sys(okm::nr_wait4, static_cast(h.h), - reinterpret_cast(&st), 0, 0); - if (okm::interrupted(r)) continue; - if (okm::failed(r)) return okm::translate(r); - break; - } - // The encoding is the kernel's: the low seven bits name the signal that - // ended the program and are zero when it ended by returning, in which case - // the next eight bits are what it returned. - const int signalled = st & 0x7f; - if (signalled == 0) { - if (status) *status = (st >> 8) & 0xff; - if (terminated_by_environment) *terminated_by_environment = 0; - } else { - if (status) *status = signalled; - if (terminated_by_environment) *terminated_by_environment = 1; - } - return kal_ok; -} +// ⭐ REACHES THE WHOLE JOB WHEN THERE IS ONE, AND THE HANDLE CARRIES NOTHING TO +// SAY SO. A program started with KAL_SPAWN_OWN_JOB called `setpgid(0, 0)', so its +// group identifier is its own; one started without it inherited this +// implementation's, which is some other process. `getpgid(pid) == pid' +// distinguishes them exactly. +// +// ⚠️ Without this the flag would do nothing a caller could see: forming the job +// matters only because terminating then reaches what the started program itself +// started. int kal_process_terminate(kal_process h) { if (h.h == 0) return kal_err_invalid; - const okm_long r = okm::sys(okm::nr_kill, static_cast(h.h), 15 /* SIGTERM */); + const okm_long pid = static_cast(h.h); + const okm_long pgid = okm::sys(nr_getpgid, pid); + const okm_long target = (!okm::failed(pgid) && pgid == pid) ? -pid : pid; + const okm_long r = okm::sys(okm::nr_kill, target, 15 /* SIGTERM */); return okm::failed(r) ? okm::translate(r) : kal_ok; } @@ -249,32 +245,11 @@ void kal_process_close(kal_process) { } // Starting a program whose lifetime is bound to this one's. Version 0.10. // -// ⚠️⚠️ REFUSED HERE, AND THE REFUSAL IS THE HONEST ANSWER RATHER THAN A GAP TO -// FILL LATER WITH SOMETHING THAT LOOKS LIKE IT. -// -// The binding openkal describes has to hold however the caller ends, including -// when it is killed outright --- and this system has no primitive that arms it -// from inside the started image. The other kernel does, in one call. -// -// ⚠️ What this system offers instead is a WATCH: a context here can be told when -// another ends and can then act. That is not the same thing and must not be -// offered as it. A watch needs a live context to notice, so a caller that is -// killed outright notices nothing and the started program survives --- which is -// precisely the failure the operation exists to remove. Composing it would move -// the defect from "refused" to "works except when it matters". -// -// ⇒ `KAL_PROCESS_PROP_BOUND_LIFETIME' is not claimed, and a caller that asks -// first is told before it depends on it. -int kal_process_spawn_bound(kal_dir, const char*, kal_uintptr, - const char**, const kal_uintptr*, kal_uintptr, - const char**, const kal_uintptr*, kal_uintptr, - const kal_spawn_streams*, kal_process*) { - return kal_err_not_supported; -} 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_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR + | KAL_PROCESS_PROP_OWN_JOB; } } From e0634b43681201adf509a3da8e12a4362afb2ce0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 21:36:36 +0800 Subject: [PATCH 2/5] Take up openkal 0.11's unit: a handle the caller holds, not a flag The identity is established at the first start --- the first member forms the group and its identifier is reported --- so nothing has to be remembered and no registry appears. `kal_process_terminate' is one program again; the unit has its own operations. --- src/process.cpp | 52 ++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/process.cpp b/src/process.cpp index 8ec69c3..351da53 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -56,9 +56,9 @@ struct vector { }; constexpr okm_long nr_fchdir = 13; -// openkal 0.11: a started program that forms a job of its own. +// openkal 0.11: the unit a started program joins. constexpr okm_long nr_setpgid = 82; -constexpr okm_long nr_getpgid = 151; + } // namespace @@ -100,8 +100,7 @@ int kal_process_spawn(const kal_spawn* how, // outright, and what this system offers instead is a WATCH, which needs a // live context to notice. Composing it would move the defect from "refused" // to "works except when it matters". - constexpr kal_uintptr can = KAL_SPAWN_OWN_JOB; - if (how->flags & ~can) return kal_err_not_supported; + if (how->flags != 0) return kal_err_not_supported; okm::terminated p(path, path_len); if (!p.ok) return kal_err_invalid; @@ -143,7 +142,11 @@ int kal_process_spawn(const kal_spawn* how, const okm_long ou = streams ? static_cast(streams->out.h) : 0; const okm_long er = streams ? static_cast(streams->err.h) : 0; - const bool job = (how->flags & KAL_SPAWN_OWN_JOB) != 0; + // ⭐ The unit, named here by a process group --- which is to say by whichever + // program formed it first. Zero for the first member; a later one is given + // the number to join. + const okm_long join = how->job ? static_cast(how->job->h) : 0; + const bool unit = how->job != nullptr; bool is_duplicate = false; const okm_long child = okm::duplicate(is_duplicate); @@ -167,7 +170,7 @@ int kal_process_spawn(const kal_spawn* how, // is named from, so this no longer has to serve both. okm::sys(nr_fchdir, w); - if (job) okm::sys(nr_setpgid, 0, 0); + if (unit) okm::sys(nr_setpgid, 0, join); okm::sys(okm::nr_execve, reinterpret_cast(whole), reinterpret_cast(args.slots), @@ -175,6 +178,10 @@ int kal_process_spawn(const kal_spawn* how, for (;;) okm::sys(okm::nr_exit, 127); } + // Written only after the start succeeded, and only when the unit was new: + // the first member's identifier IS the group's. + if (unit && join == 0) how->job->h = static_cast(child); + *out = kal_process{ static_cast(child) }; return kal_ok; } @@ -221,24 +228,29 @@ void kal_process_channel_close(kal_stream s) { // Starting a program that receives exactly the directories named. -// ⭐ REACHES THE WHOLE JOB WHEN THERE IS ONE, AND THE HANDLE CARRIES NOTHING TO -// SAY SO. A program started with KAL_SPAWN_OWN_JOB called `setpgid(0, 0)', so its -// group identifier is its own; one started without it inherited this -// implementation's, which is some other process. `getpgid(pid) == pid' -// distinguishes them exactly. -// -// ⚠️ Without this the flag would do nothing a caller could see: forming the job -// matters only because terminating then reaches what the started program itself -// started. +// One program, whatever unit it is in --- the unit has its own operation below, +// so this one's meaning never turns on how the program was started. int kal_process_terminate(kal_process h) { if (h.h == 0) return kal_err_invalid; - const okm_long pid = static_cast(h.h); - const okm_long pgid = okm::sys(nr_getpgid, pid); - const okm_long target = (!okm::failed(pgid) && pgid == pid) ? -pid : pid; - const okm_long r = okm::sys(okm::nr_kill, target, 15 /* SIGTERM */); + const okm_long r = okm::sys(okm::nr_kill, static_cast(h.h), 15 /* SIGTERM */); + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + +// Every program in the unit, including ones never held as a handle. +// +// ⚠️ A group is named by a process identifier, and those are reused: once the +// program that formed it has ended and the numbers have wrapped, this can reach +// a different group. That is what this system does, and it is recorded rather +// than hidden. +int kal_process_job_terminate(kal_job j) { + if (j.h == 0) return kal_err_invalid; + const okm_long r = okm::sys(okm::nr_kill, -static_cast(j.h), 15 /* SIGTERM */); return okm::failed(r) ? okm::translate(r) : kal_ok; } +// A group here is a number and not a resource, so there is nothing to release. +void kal_process_job_close(kal_job) { } + // Releasing the handle does not affect the program. A program that has not been // waited for continues, and this environment collects it when the caller exits. void kal_process_close(kal_process) { } @@ -250,6 +262,6 @@ 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_OWN_JOB; } + | KAL_PROCESS_PROP_JOB; } } From 67f62e4a423f89c0c746186d9627b1299ce7e163 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 22:23:55 +0800 Subject: [PATCH 3/5] Take up the unit as a handle, and record the SIGPIPE this cannot yet quiet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kal_spawn.job' and `kal_process_job_enter' are both `setpgid' here: the unit's identity is the first member's, reported back to the caller. `job_terminate' uses the signal that cannot be declined --- a unit contains programs the caller never held a handle to, so a request any member may ignore does not terminate it. ⚠️⚠️ AND A DEFECT IS RECORDED RATHER THAN GUESSED AT. openkal defines no signals, and `kal_stream_write' is required to REPORT that a stream's far end is gone --- while this kernel delivers SIGPIPE, whose default action ends the program. A C library above answers `signal(SIGPIPE, SIG_IGN)' truthfully, having nothing to set, and the program is killed anyway by a mechanism no layer between can name. openkal-linux now ignores it in one call at startup. This kernel's `sigaction' takes a structure carrying a trampoline its C library supplies, and a disposition installed with the wrong shape shows up as a program dying in a way nobody can trace --- which is the defect this note is about, arrived at from the other side. So it waits until it can be MEASURED here, and the consequence is stated in the file rather than discovered by whoever meets it. --- src/env.cpp | 29 +++++++++++++++++++++++++++++ src/process.cpp | 17 ++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/env.cpp b/src/env.cpp index 87d7a6a..416e9ec 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -18,6 +18,35 @@ void record(int argc, char** argv, char** envp) { } // namespace okm namespace { + +// ⚠️⚠️ A PROGRAM ABOVE openkal SHALL NOT BE ENDED BY SOMETHING openkal NEVER TOLD +// IT ABOUT. openkal defines no signals, and `kal_stream_write' is required to +// REPORT that the far end of a stream is gone --- while this kernel delivers +// SIGPIPE, whose default action ends the program instead. +// +// ⭐ A C library above answers `signal(SIGPIPE, SIG_IGN)' truthfully, because +// openkal has no signals and there is nothing for it to set; the program is then +// killed anyway, by a mechanism no layer between it and here can name. Ignored +// at this level because this is the only level that can. Found on the other +// implementation, fixed on both --- a divergence here would be the same defect +// with a different exit status. +// +// ⚠️ Not a policy about signals in general: this is the one an ordinary openkal +// operation provokes. +// +// ⚠️⚠️ AND IT IS NOT FIXED HERE YET, WHICH IS RECORDED RATHER THAN LEFT TO BE +// DISCOVERED. openkal-linux ignores it in one call. This kernel's `sigaction' +// takes a `struct __sigaction' carrying a TRAMPOLINE that its C library +// supplies, and a disposition installed with the wrong shape is the kind of +// mistake that shows up as a program dying in a way nobody can trace --- which is +// the defect this note is about, arrived at from the other side. +// +// ⇒ It is left until it can be MEASURED on this system. This repository already +// refuses to claim a facility it has not exercised, and a signal disposition +// installed by guesswork is exactly that. The consequence meanwhile is stated: +// a program above this implementation that writes to a stream whose far end has +// gone is ended by SIGPIPE rather than told, and no layer between it and here +// can name what happened. [[gnu::constructor(101)]] void capture(int argc, char** argv, char** envp) { if (okm::g_argv == nullptr) okm::record(argc, argv, envp); } diff --git a/src/process.cpp b/src/process.cpp index 351da53..81a063a 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -236,6 +236,18 @@ int kal_process_terminate(kal_process h) { return okm::failed(r) ? okm::translate(r) : kal_ok; } +// 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 +// unit must say so about ITSELF before it replaces itself. +int kal_process_job_enter(kal_job* j) { + if (j == nullptr) return kal_err_invalid; + const okm_long join = static_cast(j->h); + const okm_long r = okm::sys(nr_setpgid, 0, join); + if (okm::failed(r)) return okm::translate(r); + if (join == 0) j->h = static_cast(okm::sys(okm::nr_getpid)); + return kal_ok; +} + // Every program in the unit, including ones never held as a handle. // // ⚠️ A group is named by a process identifier, and those are reused: once the @@ -244,7 +256,10 @@ int kal_process_terminate(kal_process h) { // than hidden. int kal_process_job_terminate(kal_job j) { if (j.h == 0) return kal_err_invalid; - const okm_long r = okm::sys(okm::nr_kill, -static_cast(j.h), 15 /* SIGTERM */); + // The signal that cannot be declined --- see openkal-linux for the reasoning: + // a unit contains programs the caller never held a handle to, so a request any + // member may ignore does not terminate the unit. + const okm_long r = okm::sys(okm::nr_kill, -static_cast(j.h), 9 /* SIGKILL */); return okm::failed(r) ? okm::translate(r) : kal_ok; } From 1391c77c35d660ca249c9171d62568252a15deaa Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 22:43:40 +0800 Subject: [PATCH 4/5] Decline the stop-request word, for the reason the SIGPIPE note already gives Observing a request to end means installing a disposition, and this kernel's `sigaction' takes a structure carrying a trampoline its C library supplies. A disposition installed with the wrong shape shows up as a program dying in a way nobody can trace --- which is the defect the sibling note is about, met from the other side. Not claimed, so a caller that asks first is told. --- src/process.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/process.cpp b/src/process.cpp index 81a063a..5c293f1 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -236,6 +236,18 @@ int kal_process_terminate(kal_process h) { return okm::failed(r) ? okm::translate(r) : kal_ok; } +// ⚠️⚠️ NOT CLAIMED HERE, FOR THE SAME REASON THE SIGPIPE NOTE IN src/env.cpp +// GIVES. Observing a request to end means installing a disposition, and this +// kernel's `sigaction' takes a structure carrying a TRAMPOLINE its C library +// supplies. A disposition installed with the wrong shape shows up as a program +// dying in a way nobody can trace --- and a facility this repository has not +// MEASURED is exactly what it refuses to claim elsewhere. +// +// ⇒ 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; } + // 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 // unit must say so about ITSELF before it replaces itself. From ba5feb863982ca1bfef36dc92d2b7f0de57c6a92 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 23:28:46 +0800 Subject: [PATCH 5/5] Put back kal_process_wait, which a bulk edit had swallowed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ A REGEX SUBSTITUTION ACROSS A WHOLE FILE DELETED A FUNCTION I NEVER MEANT TO TOUCH, and nothing local noticed: this file still compiled, because a definition that is absent is not a compile error --- it is a link error, and only in a program that calls it. ⭐ What found it was the cross-link job in openkal-musl, three repositories away: ld64.lld: error: undefined symbol: _kal_process_wait ⇒ Restored from the branch point rather than retyped. The test file is updated for the 0.11 spawn record at the same time. The lesson is recorded rather than resolved: a multi-line pattern applied to a whole file can remove more than it matches, and the only thing that reports it is something that LINKS. This repository builds a library and does not link a program, so its own build could never have caught this. --- src/process.cpp | 24 ++++++++++++++++++++++++ tests/conformance_process_task.cpp | 15 +++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/process.cpp b/src/process.cpp index 5c293f1..02041c4 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -229,6 +229,30 @@ void kal_process_channel_close(kal_stream s) { // Starting a program that receives exactly the directories named. // One program, whatever unit it is in --- the unit has its own operation below, +int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) { + if (h.h == 0) return kal_err_invalid; + int st = 0; + for (;;) { + const okm_long r = okm::sys(okm::nr_wait4, static_cast(h.h), + reinterpret_cast(&st), 0, 0); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return okm::translate(r); + break; + } + // The encoding is the kernel's: the low seven bits name the signal that + // ended the program and are zero when it ended by returning, in which case + // the next eight bits are what it returned. + const int signalled = st & 0x7f; + if (signalled == 0) { + if (status) *status = (st >> 8) & 0xff; + if (terminated_by_environment) *terminated_by_environment = 0; + } else { + if (status) *status = signalled; + if (terminated_by_environment) *terminated_by_environment = 1; + } + return kal_ok; +} + // so this one's meaning never turns on how the program was started. int kal_process_terminate(kal_process h) { if (h.h == 0) return kal_err_invalid; diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index cffe6e4..9517415 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -63,6 +63,11 @@ int main() { check(have_root, "a directory covering the file system is supplied"); if (have_root) { + // How every start below is described. `work' is the same directory as + // `base' --- a caller that does not care passes it, openkal having no + // ambient working directory for a default to mean. + const kal_spawn how{ slash, slash, nullptr, nullptr, 0, 0 }; + // The program that succeeds and the program that fails are at // different places on different systems. The test locates them rather // than assuming, because assuming would make it a test of one system. @@ -104,7 +109,7 @@ int main() { const kal_uintptr lens[] = { 7 }; int rc = kal_err_invalid; if (t >= 0) - rc = kal_process_spawn(slash, true_paths[t], true_lens[t], argv, lens, 1, + rc = kal_process_spawn(&how, true_paths[t], true_lens[t], argv, lens, 1, nullptr, nullptr, 0, nullptr, &p); check(rc == kal_ok, "a program is started"); if (rc == kal_ok) { @@ -128,7 +133,7 @@ int main() { const char* qargv[] = { "openkal" }; int qrc = kal_err_invalid; if (fpath >= 0) - qrc = kal_process_spawn(slash, false_paths[fpath], false_lens[fpath], qargv, lens, 1, + qrc = kal_process_spawn(&how, false_paths[fpath], false_lens[fpath], qargv, lens, 1, nullptr, nullptr, 0, nullptr, &q); check(qrc == kal_ok, "the program that fails is started"); if (qrc == kal_ok) { @@ -171,7 +176,7 @@ int main() { const kal_uintptr rlens[] = { 22, 2, script_len }; int rrc = kal_err_invalid; if (sh >= 0) - rrc = kal_process_spawn(slash, sh_paths[sh], sh_lens[sh], rargv, rlens, 3, + rrc = kal_process_spawn(&how, sh_paths[sh], sh_lens[sh], rargv, rlens, 3, nullptr, nullptr, 0, nullptr, &r); check(rrc == kal_ok, "a shell is started"); if (rrc == kal_ok) { @@ -185,7 +190,9 @@ int main() { // A name that ascends is refused here as it is in the file system. kal_process bad{}; - check(kal_process_spawn(kal::fs::working(), "../bin/true", 11, + const kal_spawn escape{ kal::fs::working(), kal::fs::working(), + nullptr, nullptr, 0, 0 }; + check(kal_process_spawn(&escape, "../bin/true", 11, nullptr, nullptr, 0, nullptr, nullptr, 0, nullptr, &bad) != kal_ok, "an ascending program name is refused");