Skip to content

Commit 0592e26

Browse files
authored
Take up openkal 0.11's unit: a handle the caller holds, not a flag (#17)
* 0.8.0 --- one spawn, and the divergence this implementation had been hiding ⚠️⚠️ 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. * 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. * Take up the unit as a handle, and record the SIGPIPE this cannot yet quiet `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. * 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. * Put back kal_process_wait, which a bulk edit had swallowed ⚠️⚠️ 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.
1 parent 1bbed0f commit 0592e26

4 files changed

Lines changed: 175 additions & 101 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.7.0"
4+
version = "0.8.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.10.0"
21+
openkal = "0.11.0"
2222

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

src/env.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,35 @@ void record(int argc, char** argv, char** envp) {
1818
} // namespace okm
1919

2020
namespace {
21+
22+
// ⚠️⚠️ A PROGRAM ABOVE openkal SHALL NOT BE ENDED BY SOMETHING openkal NEVER TOLD
23+
// IT ABOUT. openkal defines no signals, and `kal_stream_write' is required to
24+
// REPORT that the far end of a stream is gone --- while this kernel delivers
25+
// SIGPIPE, whose default action ends the program instead.
26+
//
27+
// ⭐ A C library above answers `signal(SIGPIPE, SIG_IGN)' truthfully, because
28+
// openkal has no signals and there is nothing for it to set; the program is then
29+
// killed anyway, by a mechanism no layer between it and here can name. Ignored
30+
// at this level because this is the only level that can. Found on the other
31+
// implementation, fixed on both --- a divergence here would be the same defect
32+
// with a different exit status.
33+
//
34+
// ⚠️ Not a policy about signals in general: this is the one an ordinary openkal
35+
// operation provokes.
36+
//
37+
// ⚠️⚠️ AND IT IS NOT FIXED HERE YET, WHICH IS RECORDED RATHER THAN LEFT TO BE
38+
// DISCOVERED. openkal-linux ignores it in one call. This kernel's `sigaction'
39+
// takes a `struct __sigaction' carrying a TRAMPOLINE that its C library
40+
// supplies, and a disposition installed with the wrong shape is the kind of
41+
// mistake that shows up as a program dying in a way nobody can trace --- which is
42+
// the defect this note is about, arrived at from the other side.
43+
//
44+
// ⇒ It is left until it can be MEASURED on this system. This repository already
45+
// refuses to claim a facility it has not exercised, and a signal disposition
46+
// installed by guesswork is exactly that. The consequence meanwhile is stated:
47+
// a program above this implementation that writes to a stream whose far end has
48+
// gone is ended by SIGPIPE rather than told, and no layer between it and here
49+
// can name what happened.
2150
[[gnu::constructor(101)]] void capture(int argc, char** argv, char** envp) {
2251
if (okm::g_argv == nullptr) okm::record(argc, argv, envp);
2352
}

src/process.cpp

Lines changed: 133 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -56,35 +56,98 @@ struct vector {
5656
};
5757

5858
constexpr okm_long nr_fchdir = 13;
59+
// openkal 0.11: the unit a started program joins.
60+
constexpr okm_long nr_setpgid = 82;
61+
5962

6063
} // namespace
6164

6265
extern "C" {
6366

64-
int kal_process_spawn(kal_dir base,
67+
// Starting a program. One function since openkal 0.11, where three declarations
68+
// became one and their modifiers became positions in `kal_spawn'.
69+
//
70+
// ⚠️⚠️ AND THIS IMPLEMENTATION IS WHERE THE MISSING MODIFIER WAS ALREADY VISIBLE,
71+
// which is worth recording rather than quietly fixing.
72+
//
73+
// This kernel has no `execveat', so a program named relative to a directory has
74+
// always been started by entering that directory first --- the `fchdir(base)'
75+
// below used to be the whole story. So a started program's working directory WAS
76+
// `base' here, and on the other kernel it was whatever that implementation
77+
// happened to be in. ⭐ Same openkal calls, two different observable answers,
78+
// and neither was wrong because the specification said nothing.
79+
//
80+
// ⇒ 0.11 gives the caller a second directory, and both implementations now enter
81+
// the one the caller named. The divergence is gone because the thing that caused
82+
// it --- a property nobody had declared --- is declared.
83+
int kal_process_spawn(const kal_spawn* how,
6584
const char* path, kal_uintptr path_len,
6685
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
6786
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
6887
const kal_spawn_streams* streams,
6988
kal_process* out) {
70-
const int b = okm::unpack(base.h);
71-
if (b < 0 || out == nullptr) return kal_err_invalid;
89+
if (how == nullptr || out == nullptr) return kal_err_invalid;
90+
91+
const int b = okm::unpack(how->base.h);
92+
const int w = okm::unpack(how->work.h);
93+
if (b < 0 || w < 0) return kal_err_invalid;
7294
if (!okm::acceptable(path, path_len)) return kal_err_invalid;
95+
if (how->grant_count > 0 && how->grants == nullptr) return kal_err_invalid;
96+
97+
// ⚠️ A LIFETIME THIS KERNEL CANNOT BIND IS REFUSED BEFORE ANYTHING STARTS.
98+
// The reason is unchanged from 0.10 and is stated at kal_process_props: the
99+
// binding must hold however the caller ends, including when it is killed
100+
// outright, and what this system offers instead is a WATCH, which needs a
101+
// live context to notice. Composing it would move the defect from "refused"
102+
// to "works except when it matters".
103+
if (how->flags != 0) return kal_err_not_supported;
104+
73105
okm::terminated p(path, path_len);
74106
if (!p.ok) return kal_err_invalid;
75107

76-
// The vector is passed unaltered. Clause 7.6: argv[0] is the name the
77-
// started program observes as its own, and it is the caller's to choose ---
78-
// the started program reads it through kal_env_arg(0), so a caller that did
79-
// not supply it could not predict what the program would read.
80108
vector args, envs;
81109
if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory;
82110
if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory;
83111

112+
constexpr kal_uintptr max_grants = 16;
113+
if (how->grant_count > max_grants) return kal_err_invalid;
114+
int granted[max_grants];
115+
for (kal_uintptr i = 0; i < how->grant_count; ++i) {
116+
granted[i] = okm::unpack(how->grants[i].dir.h);
117+
if (granted[i] < 0) return kal_err_invalid;
118+
}
119+
120+
// ⭐ THE PROGRAM'S NAME IS MADE ABSOLUTE BEFORE THE DIRECTORY MOVES, because
121+
// with no `execveat' the two things `base' and `work' now mean cannot both be
122+
// served by one `fchdir'. `F_GETPATH' answers the path of an open directory,
123+
// which src/fs.cpp already relies on for the same reason: this kernel has no
124+
// call that reports a working directory, so a path is obtained from the
125+
// descriptor that names it.
126+
char whole[1024];
127+
kal_uintptr n = 0;
128+
if (p.buf[0] == '/') {
129+
while (p.buf[n] && n < sizeof whole - 1) { whole[n] = p.buf[n]; ++n; }
130+
} else {
131+
const okm_long r = okm::sys(okm::nr_fcntl, b, okm::f_getpath,
132+
reinterpret_cast<okm_long>(whole));
133+
if (okm::failed(r)) return okm::translate(r);
134+
while (whole[n] && n < sizeof whole - 1) ++n;
135+
if (n && whole[n - 1] != '/' && n < sizeof whole - 1) whole[n++] = '/';
136+
for (kal_uintptr i = 0; p.buf[i] && n < sizeof whole - 1; ++i) whole[n++] = p.buf[i];
137+
}
138+
if (n >= sizeof whole - 1) return kal_err_invalid;
139+
whole[n] = '\0';
140+
84141
const okm_long in = streams ? static_cast<okm_long>(streams->in.h) : 0;
85142
const okm_long ou = streams ? static_cast<okm_long>(streams->out.h) : 0;
86143
const okm_long er = streams ? static_cast<okm_long>(streams->err.h) : 0;
87144

145+
// ⭐ The unit, named here by a process group --- which is to say by whichever
146+
// program formed it first. Zero for the first member; a later one is given
147+
// the number to join.
148+
const okm_long join = how->job ? static_cast<okm_long>(how->job->h) : 0;
149+
const bool unit = how->job != nullptr;
150+
88151
bool is_duplicate = false;
89152
const okm_long child = okm::duplicate(is_duplicate);
90153
if (okm::failed(child)) return okm::translate(child);
@@ -97,13 +160,28 @@ int kal_process_spawn(kal_dir base,
97160
if (in != 0) okm::sys(okm::nr_dup2, in, 0);
98161
if (ou != 0) okm::sys(okm::nr_dup2, ou, 1);
99162
if (er != 0) okm::sys(okm::nr_dup2, er, 2);
100-
okm::sys(nr_fchdir, b);
101-
okm::sys(okm::nr_execve, reinterpret_cast<okm_long>(p.buf),
163+
164+
for (kal_uintptr i = 0; i < how->grant_count; ++i) {
165+
const okm_long want = static_cast<okm_long>(3 + i);
166+
if (granted[i] != want) okm::sys(okm::nr_dup2, granted[i], want);
167+
}
168+
169+
// The directory the program RUNS in --- `whole' already carries where it
170+
// is named from, so this no longer has to serve both.
171+
okm::sys(nr_fchdir, w);
172+
173+
if (unit) okm::sys(nr_setpgid, 0, join);
174+
175+
okm::sys(okm::nr_execve, reinterpret_cast<okm_long>(whole),
102176
reinterpret_cast<okm_long>(args.slots),
103177
reinterpret_cast<okm_long>(envs.slots));
104178
for (;;) okm::sys(okm::nr_exit, 127);
105179
}
106180

181+
// Written only after the start succeeded, and only when the unit was new:
182+
// the first member's identifier IS the group's.
183+
if (unit && join == 0) how->job->h = static_cast<kal_uintptr>(child);
184+
107185
*out = kal_process{ static_cast<kal_uintptr>(child) };
108186
return kal_ok;
109187
}
@@ -149,70 +227,8 @@ void kal_process_channel_close(kal_stream s) {
149227
}
150228

151229
// Starting a program that receives exactly the directories named.
152-
//
153-
// The grants are placed as descriptors three and upward, which is where
154-
// kal_fs_preopen reads them back from. The inverse relationship clause 7.11
155-
// describes is between those two operations, which is why they must agree about
156-
// the numbering rather than each choosing one.
157-
int kal_process_spawn_with(kal_dir base,
158-
const char* path, kal_uintptr path_len,
159-
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
160-
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
161-
const kal_spawn_streams* streams,
162-
const kal_preopen* grants, kal_uintptr grant_count,
163-
kal_process* out) {
164-
const int b = okm::unpack(base.h);
165-
if (b < 0 || out == nullptr) return kal_err_invalid;
166-
if (!okm::acceptable(path, path_len)) return kal_err_invalid;
167-
if (grant_count > 0 && grants == nullptr) return kal_err_invalid;
168-
okm::terminated p(path, path_len);
169-
if (!p.ok) return kal_err_invalid;
170-
171-
vector args, envs;
172-
if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory;
173-
if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory;
174-
175-
// Resolved before the duplication, because a failure after it would leave a
176-
// child to be reaped and a caller holding an error it cannot act upon.
177-
constexpr kal_uintptr max_grants = 16;
178-
if (grant_count > max_grants) return kal_err_invalid;
179-
int granted[max_grants];
180-
for (kal_uintptr i = 0; i < grant_count; ++i) {
181-
granted[i] = okm::unpack(grants[i].dir.h);
182-
if (granted[i] < 0) return kal_err_invalid;
183-
}
184-
185-
const okm_long in = streams ? static_cast<okm_long>(streams->in.h) : 0;
186-
const okm_long ou = streams ? static_cast<okm_long>(streams->out.h) : 0;
187-
const okm_long er = streams ? static_cast<okm_long>(streams->err.h) : 0;
188-
189-
bool is_duplicate = false;
190-
const okm_long child = okm::duplicate(is_duplicate);
191-
if (okm::failed(child)) return okm::translate(child);
192-
193-
if (is_duplicate) {
194-
if (in != 0) okm::sys(okm::nr_dup2, in, 0);
195-
if (ou != 0) okm::sys(okm::nr_dup2, ou, 1);
196-
if (er != 0) okm::sys(okm::nr_dup2, er, 2);
197-
198-
// dup2 onto the same number succeeds and does nothing, unlike dup3,
199-
// which refuses. Either behaviour is right for this loop; only the
200-
// reason differs, and it is stated so that a reader comparing the two
201-
// implementations does not take one of them for an oversight.
202-
for (kal_uintptr i = 0; i < grant_count; ++i)
203-
okm::sys(okm::nr_dup2, granted[i], static_cast<okm_long>(3 + i));
204-
205-
okm::sys(nr_fchdir, b);
206-
okm::sys(okm::nr_execve, reinterpret_cast<okm_long>(p.buf),
207-
reinterpret_cast<okm_long>(args.slots),
208-
reinterpret_cast<okm_long>(envs.slots));
209-
for (;;) okm::sys(okm::nr_exit, 127);
210-
}
211-
212-
*out = kal_process{ static_cast<kal_uintptr>(child) };
213-
return kal_ok;
214-
}
215230

231+
// One program, whatever unit it is in --- the unit has its own operation below,
216232
int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) {
217233
if (h.h == 0) return kal_err_invalid;
218234
int st = 0;
@@ -237,44 +253,66 @@ int kal_process_wait(kal_process h, int* status, int* terminated_by_environment)
237253
return kal_ok;
238254
}
239255

256+
// so this one's meaning never turns on how the program was started.
240257
int kal_process_terminate(kal_process h) {
241258
if (h.h == 0) return kal_err_invalid;
242259
const okm_long r = okm::sys(okm::nr_kill, static_cast<okm_long>(h.h), 15 /* SIGTERM */);
243260
return okm::failed(r) ? okm::translate(r) : kal_ok;
244261
}
245262

263+
// ⚠️⚠️ NOT CLAIMED HERE, FOR THE SAME REASON THE SIGPIPE NOTE IN src/env.cpp
264+
// GIVES. Observing a request to end means installing a disposition, and this
265+
// kernel's `sigaction' takes a structure carrying a TRAMPOLINE its C library
266+
// supplies. A disposition installed with the wrong shape shows up as a program
267+
// dying in a way nobody can trace --- and a facility this repository has not
268+
// MEASURED is exactly what it refuses to claim elsewhere.
269+
//
270+
// ⇒ Null, and KAL_PROCESS_PROP_STOP_REQUESTED unclaimed, so a caller that asks
271+
// first is told. The other implementation answers it; this one will when it can
272+
// be exercised here.
273+
const kal_u32* kal_process_stop_requested(void) { return 0; }
274+
275+
// This program itself joins or forms a unit --- what `kal_spawn.job' cannot say,
276+
// because that places a program the caller STARTS and a copy wishing to lead a
277+
// unit must say so about ITSELF before it replaces itself.
278+
int kal_process_job_enter(kal_job* j) {
279+
if (j == nullptr) return kal_err_invalid;
280+
const okm_long join = static_cast<okm_long>(j->h);
281+
const okm_long r = okm::sys(nr_setpgid, 0, join);
282+
if (okm::failed(r)) return okm::translate(r);
283+
if (join == 0) j->h = static_cast<kal_uintptr>(okm::sys(okm::nr_getpid));
284+
return kal_ok;
285+
}
286+
287+
// Every program in the unit, including ones never held as a handle.
288+
//
289+
// ⚠️ A group is named by a process identifier, and those are reused: once the
290+
// program that formed it has ended and the numbers have wrapped, this can reach
291+
// a different group. That is what this system does, and it is recorded rather
292+
// than hidden.
293+
int kal_process_job_terminate(kal_job j) {
294+
if (j.h == 0) return kal_err_invalid;
295+
// The signal that cannot be declined --- see openkal-linux for the reasoning:
296+
// a unit contains programs the caller never held a handle to, so a request any
297+
// member may ignore does not terminate the unit.
298+
const okm_long r = okm::sys(okm::nr_kill, -static_cast<okm_long>(j.h), 9 /* SIGKILL */);
299+
return okm::failed(r) ? okm::translate(r) : kal_ok;
300+
}
301+
302+
// A group here is a number and not a resource, so there is nothing to release.
303+
void kal_process_job_close(kal_job) { }
304+
246305
// Releasing the handle does not affect the program. A program that has not been
247306
// waited for continues, and this environment collects it when the caller exits.
248307
void kal_process_close(kal_process) { }
249308

250309
// Starting a program whose lifetime is bound to this one's. Version 0.10.
251310
//
252-
// ⚠️⚠️ REFUSED HERE, AND THE REFUSAL IS THE HONEST ANSWER RATHER THAN A GAP TO
253-
// FILL LATER WITH SOMETHING THAT LOOKS LIKE IT.
254-
//
255-
// The binding openkal describes has to hold however the caller ends, including
256-
// when it is killed outright --- and this system has no primitive that arms it
257-
// from inside the started image. The other kernel does, in one call.
258-
//
259-
// ⚠️ What this system offers instead is a WATCH: a context here can be told when
260-
// another ends and can then act. That is not the same thing and must not be
261-
// offered as it. A watch needs a live context to notice, so a caller that is
262-
// killed outright notices nothing and the started program survives --- which is
263-
// precisely the failure the operation exists to remove. Composing it would move
264-
// the defect from "refused" to "works except when it matters".
265-
//
266-
// ⇒ `KAL_PROCESS_PROP_BOUND_LIFETIME' is not claimed, and a caller that asks
267-
// first is told before it depends on it.
268-
int kal_process_spawn_bound(kal_dir, const char*, kal_uintptr,
269-
const char**, const kal_uintptr*, kal_uintptr,
270-
const char**, const kal_uintptr*, kal_uintptr,
271-
const kal_spawn_streams*, kal_process*) {
272-
return kal_err_not_supported;
273-
}
274311

275312
kal_uintptr kal_process_props(void) { return
276313
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
277314
| KAL_PROCESS_PROP_EXIT_STATUS
278-
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; }
315+
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR
316+
| KAL_PROCESS_PROP_JOB; }
279317

280318
}

0 commit comments

Comments
 (0)