Skip to content

Commit 8d09318

Browse files
committed
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.
1 parent 1bbed0f commit 8d09318

2 files changed

Lines changed: 97 additions & 122 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/process.cpp

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

5858
constexpr okm_long nr_fchdir = 13;
59+
// openkal 0.11: a started program that forms a job of its own.
60+
constexpr okm_long nr_setpgid = 82;
61+
constexpr okm_long nr_getpgid = 151;
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+
constexpr kal_uintptr can = KAL_SPAWN_OWN_JOB;
104+
if (how->flags & ~can) return kal_err_not_supported;
105+
73106
okm::terminated p(path, path_len);
74107
if (!p.ok) return kal_err_invalid;
75108

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.
80109
vector args, envs;
81110
if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory;
82111
if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory;
83112

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

146+
const bool job = (how->flags & KAL_SPAWN_OWN_JOB) != 0;
147+
88148
bool is_duplicate = false;
89149
const okm_long child = okm::duplicate(is_duplicate);
90150
if (okm::failed(child)) return okm::translate(child);
@@ -97,8 +157,19 @@ int kal_process_spawn(kal_dir base,
97157
if (in != 0) okm::sys(okm::nr_dup2, in, 0);
98158
if (ou != 0) okm::sys(okm::nr_dup2, ou, 1);
99159
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),
160+
161+
for (kal_uintptr i = 0; i < how->grant_count; ++i) {
162+
const okm_long want = static_cast<okm_long>(3 + i);
163+
if (granted[i] != want) okm::sys(okm::nr_dup2, granted[i], want);
164+
}
165+
166+
// The directory the program RUNS in --- `whole' already carries where it
167+
// is named from, so this no longer has to serve both.
168+
okm::sys(nr_fchdir, w);
169+
170+
if (job) okm::sys(nr_setpgid, 0, 0);
171+
172+
okm::sys(okm::nr_execve, reinterpret_cast<okm_long>(whole),
102173
reinterpret_cast<okm_long>(args.slots),
103174
reinterpret_cast<okm_long>(envs.slots));
104175
for (;;) okm::sys(okm::nr_exit, 127);
@@ -149,97 +220,22 @@ void kal_process_channel_close(kal_stream s) {
149220
}
150221

151222
// 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-
}
215-
216-
int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) {
217-
if (h.h == 0) return kal_err_invalid;
218-
int st = 0;
219-
for (;;) {
220-
const okm_long r = okm::sys(okm::nr_wait4, static_cast<okm_long>(h.h),
221-
reinterpret_cast<okm_long>(&st), 0, 0);
222-
if (okm::interrupted(r)) continue;
223-
if (okm::failed(r)) return okm::translate(r);
224-
break;
225-
}
226-
// The encoding is the kernel's: the low seven bits name the signal that
227-
// ended the program and are zero when it ended by returning, in which case
228-
// the next eight bits are what it returned.
229-
const int signalled = st & 0x7f;
230-
if (signalled == 0) {
231-
if (status) *status = (st >> 8) & 0xff;
232-
if (terminated_by_environment) *terminated_by_environment = 0;
233-
} else {
234-
if (status) *status = signalled;
235-
if (terminated_by_environment) *terminated_by_environment = 1;
236-
}
237-
return kal_ok;
238-
}
239223

224+
// ⭐ REACHES THE WHOLE JOB WHEN THERE IS ONE, AND THE HANDLE CARRIES NOTHING TO
225+
// SAY SO. A program started with KAL_SPAWN_OWN_JOB called `setpgid(0, 0)', so its
226+
// group identifier is its own; one started without it inherited this
227+
// implementation's, which is some other process. `getpgid(pid) == pid'
228+
// distinguishes them exactly.
229+
//
230+
// ⚠️ Without this the flag would do nothing a caller could see: forming the job
231+
// matters only because terminating then reaches what the started program itself
232+
// started.
240233
int kal_process_terminate(kal_process h) {
241234
if (h.h == 0) return kal_err_invalid;
242-
const okm_long r = okm::sys(okm::nr_kill, static_cast<okm_long>(h.h), 15 /* SIGTERM */);
235+
const okm_long pid = static_cast<okm_long>(h.h);
236+
const okm_long pgid = okm::sys(nr_getpgid, pid);
237+
const okm_long target = (!okm::failed(pgid) && pgid == pid) ? -pid : pid;
238+
const okm_long r = okm::sys(okm::nr_kill, target, 15 /* SIGTERM */);
243239
return okm::failed(r) ? okm::translate(r) : kal_ok;
244240
}
245241

@@ -249,32 +245,11 @@ void kal_process_close(kal_process) { }
249245

250246
// Starting a program whose lifetime is bound to this one's. Version 0.10.
251247
//
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-
}
274248

275249
kal_uintptr kal_process_props(void) { return
276250
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
277251
| KAL_PROCESS_PROP_EXIT_STATUS
278-
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; }
252+
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR
253+
| KAL_PROCESS_PROP_OWN_JOB; }
279254

280255
}

0 commit comments

Comments
 (0)