Skip to content

Commit 3f16c78

Browse files
authored
0.8.0 --- a replacement that failed is reported to the caller, not to nobody (#21)
Closes #20. ⚠️⚠️ THE REPLACEMENT HAPPENS IN THE DUPLICATE, SO ITS FAILURE HAD NOWHERE TO GO. A program is started here by duplicating this image and replacing the duplicate. The replacement is the part that fails --- the name is absent, or is a directory, or is not a program, or may not be executed --- and it failed inside an image the caller does not have. This implementation ended that image with 127 and answered `kal_ok' with a handle, so a caller learned something was wrong only by waiting and reading 127, which is exactly what a program that RAN and exited 127 reports. ⭐ WHAT IT COST, MEASURED BY A CONSUMER RATHER THAN HERE. openkal-musl expresses `execve' as starting a program and ending with its status, so a name that could not be started ended the CALLING program with 127 instead of returning -1. musl's `execvp' issues one `execve' per PATH entry and needs each to return, so the search could not survive its first miss --- `bwrap', installed at /usr/bin/bwrap, was reported as not installed. openkal-linux#13, nine of nineteen test failures. openkal-musl 0.10.0 answered the half it could, by asking `kal_fs_info' whether the name is there. It cannot answer the other half: openkal reports no execute permission, so "present and not a program" is invisible above this line. It was never invisible HERE --- the duplicate knows precisely why --- and this release is the channel that carries it. --- what it is ------------------------------------------------------------- A pipe whose ends close when the image is replaced. Nothing arrives ⇒ the replacement happened. A value arrives ⇒ it did not, and the value says why; this image then waits for the duplicate so nothing is left for a caller to meet. ⚠️ AND THE PIPE MUST NOT SIT WHERE THE DUPLICATE IS ABOUT TO PLACE SOMETHING. Streams go to 0, 1 and 2 and granted directories to 3 upwards, so a pipe holding one of those numbers would be closed by the very placement whose failure it exists to report --- and this image would read end-of-input and call that success. `F_DUPFD_CLOEXEC' answers the lowest FREE descriptor at or above a bound, which is the primitive for moving it aside without NAMING --- and so without closing --- a descriptor the caller holds. `dup3' cannot: it is told the number. ⭐ A pipe that cannot be made is not a reason to refuse a spawn. The channel is an improvement on the answer that follows it, not a precondition of it. --- measured, with the released version as the control ---------------------- A file that exists, is not a program, and is named to `posix_spawn' and `execve' through openkal-musl: 0.7.1 spawn reported SUCCESS, child status 0x7f00, and the probe then died at 127 on the execve --- two observations red and the rest unreached here refused with EACCES, and execve RETURNED with EACCES `nr_fcntl' is added for both architectures and `f_dupfd_cloexec' beside `o_cloexec'; nothing else in the surface changes.
1 parent 44ba11a commit 3f16c78

4 files changed

Lines changed: 154 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ for Linux, written on the kernel's own system-call interface.
88
openkal = "0.9.0"
99

1010
[target.'cfg(os = "linux")'.dependencies]
11-
openkal-linux = "0.7.1"
11+
openkal-linux = "0.8.0"
1212
```
1313

1414
## Why it does not use a C library

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-linux"
4-
version = "0.7.1"
4+
version = "0.8.0"
55
description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one."
66
license = "Apache-2.0"
77

‎src/process.cpp‎

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,115 @@ struct vector {
4646
}
4747
};
4848

49+
// --- reporting a replacement that failed -----------------------------------
50+
//
51+
// ⚠️⚠️ THE REPLACEMENT HAPPENS IN THE DUPLICATE, SO ITS FAILURE WAS REPORTED TO
52+
// NOBODY.
53+
//
54+
// A program is started here by duplicating this image and replacing the
55+
// duplicate. The replacement is the part that can fail --- the name is absent,
56+
// or is a directory, or is not a program, or may not be executed --- and it
57+
// fails inside an image the caller does not have. This implementation ended
58+
// that image with 127 and answered `kal_ok' with a handle, so a caller learned
59+
// something was wrong only by waiting and reading 127, which is exactly what a
60+
// program that RAN and exited 127 reports.
61+
//
62+
// ⭐ WHAT THAT COST, MEASURED BY A CONSUMER RATHER THAN HERE. openkal-musl
63+
// expresses `execve' as starting a program and ending with its status, so a
64+
// name that could not be started ended the CALLING program with 127 instead of
65+
// returning -1. musl's `execvp' issues one `execve' per PATH entry and needs
66+
// each to return, so the search could not survive its first miss: `bwrap',
67+
// installed at /usr/bin/bwrap, was reported as not installed. openkal-linux#13,
68+
// nine of nineteen test failures.
69+
//
70+
// openkal-musl 0.10.0 answers the part it can --- it asks `kal_fs_info' whether
71+
// the name is there before starting. It cannot answer the rest: openkal reports
72+
// no execute permission, so "present and not executable" is invisible above
73+
// this line. It is not invisible HERE. The duplicate knows precisely why, and
74+
// this is the channel that carries it.
75+
//
76+
// The arrangement is the ordinary one: a pipe whose ends close when the image is
77+
// replaced. Nothing arrives ⇒ the replacement happened. A value arrives ⇒ it did
78+
// not, and the value says why.
79+
struct exec_report {
80+
int fd[2] = { -1, -1 };
81+
bool armed = false;
82+
83+
// ⚠️ THE PIPE MUST NOT SIT WHERE THE DUPLICATE IS ABOUT TO PLACE SOMETHING.
84+
// The duplicate places streams at 0, 1 and 2 and granted directories at 3
85+
// and upwards, so a pipe that happened to hold one of those numbers would be
86+
// closed by the very placement whose failure it exists to report --- and the
87+
// parent would then read end-of-input and call that success.
88+
//
89+
// `F_DUPFD_CLOEXEC' answers the lowest FREE descriptor at or above a bound.
90+
// That is the primitive for this, and `dup3' is not: `dup3' is told the
91+
// number and closes whatever the caller had on it.
92+
bool open(kal_uintptr placements) {
93+
const okl_long r = okl::sys(okl::nr_pipe2,
94+
reinterpret_cast<okl_long>(fd), okl::o_cloexec);
95+
if (okl::failed(r)) return false;
96+
const okl_long floor = 3 + static_cast<okl_long>(placements);
97+
armed = lift(fd[0], floor) && lift(fd[1], floor);
98+
if (!armed) close_both();
99+
return armed;
100+
}
101+
102+
void close_both() {
103+
if (fd[0] >= 0) okl::sys(okl::nr_close, fd[0]);
104+
if (fd[1] >= 0) okl::sys(okl::nr_close, fd[1]);
105+
fd[0] = fd[1] = -1;
106+
}
107+
108+
// In the duplicate, once the replacement has returned --- which it does only
109+
// when it did not happen.
110+
void say(okl_long failure) const {
111+
if (!armed) return;
112+
okl_long value = failure;
113+
okl::sys(okl::nr_write, fd[1],
114+
reinterpret_cast<okl_long>(&value), sizeof value);
115+
}
116+
117+
// In this image. Zero when the replacement happened, otherwise the kernel's
118+
// own negative value for why it did not.
119+
okl_long heard() {
120+
if (!armed) return 0;
121+
okl::sys(okl::nr_close, fd[1]);
122+
fd[1] = -1;
123+
okl_long value = 0;
124+
okl_long n;
125+
// A transfer this short is not divided, but it can be interrupted.
126+
do {
127+
n = okl::sys(okl::nr_read, fd[0],
128+
reinterpret_cast<okl_long>(&value), sizeof value);
129+
} while (n == -okl::e_intr);
130+
okl::sys(okl::nr_close, fd[0]);
131+
fd[0] = -1;
132+
return (n == static_cast<okl_long>(sizeof value)) ? value : 0;
133+
}
134+
135+
private:
136+
static bool lift(int& f, okl_long floor) {
137+
const okl_long n = okl::sys(okl::nr_fcntl, f, okl::f_dupfd_cloexec, floor);
138+
if (okl::failed(n)) return false;
139+
okl::sys(okl::nr_close, f);
140+
f = static_cast<int>(n);
141+
return true;
142+
}
143+
};
144+
145+
// A duplicate that could not be replaced is ended, and this image waits for it
146+
// so that nothing is left for a caller to meet later. It is the one wait this
147+
// implementation performs that a caller did not ask for, and it is bounded: the
148+
// duplicate has already reached `exit_group'.
149+
inline void reap(okl_long child) {
150+
int status = 0;
151+
okl_long r;
152+
do {
153+
r = okl::sys(okl::nr_wait4, child,
154+
reinterpret_cast<okl_long>(&status), 0, 0);
155+
} while (r == -okl::e_intr);
156+
}
157+
49158
} // namespace
50159

51160
extern "C" {
@@ -74,12 +183,18 @@ int kal_process_spawn(kal_dir base,
74183
const okl_long ou = streams ? static_cast<okl_long>(streams->out.h) : 0;
75184
const okl_long er = streams ? static_cast<okl_long>(streams->err.h) : 0;
76185

186+
// Opened before the duplication, so that the duplicate inherits it. A pipe
187+
// that cannot be made is not a reason to refuse the spawn: this answers as
188+
// it did before the channel existed.
189+
exec_report report;
190+
report.open(0);
191+
77192
// The image is duplicated and then replaced. openkal has no operation that
78193
// duplicates the calling image, and this is why: the duplicate is not a
79194
// resource the caller receives, it exists for the length of two system
80195
// calls, and no environment without it could be asked to reproduce it.
81196
const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0);
82-
if (okl::failed(child)) return okl::translate(child);
197+
if (okl::failed(child)) { report.close_both(); return okl::translate(child); }
83198

84199
if (child == 0) {
85200
if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0);
@@ -90,13 +205,22 @@ int kal_process_spawn(kal_dir base,
90205
// operation that changes a working directory afterwards, because a
91206
// working directory that can be changed is shared mutable state
92207
// between execution contexts.
93-
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
94-
reinterpret_cast<okl_long>(args.slots),
95-
reinterpret_cast<okl_long>(envs.slots), 0);
208+
const okl_long why =
209+
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
210+
reinterpret_cast<okl_long>(args.slots),
211+
reinterpret_cast<okl_long>(envs.slots), 0);
212+
// Reached only when the replacement did not happen, because when it does
213+
// there is nothing here to reach.
214+
report.say(why);
96215
okl::sys(okl::nr_exit_group, 127);
97216
for (;;) { }
98217
}
99218

219+
if (const okl_long why = report.heard()) {
220+
reap(child);
221+
return okl::translate(why);
222+
}
223+
100224
*out = kal_process{ static_cast<kal_uintptr>(child) };
101225
return kal_ok;
102226
}
@@ -187,8 +311,12 @@ int kal_process_spawn_with(kal_dir base,
187311
const okl_long ou = streams ? static_cast<okl_long>(streams->out.h) : 0;
188312
const okl_long er = streams ? static_cast<okl_long>(streams->err.h) : 0;
189313

314+
// The bound is 3 + grant_count, because the placements below reach that far.
315+
exec_report report;
316+
report.open(grant_count);
317+
190318
const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0);
191-
if (okl::failed(child)) return okl::translate(child);
319+
if (okl::failed(child)) { report.close_both(); return okl::translate(child); }
192320

193321
if (child == 0) {
194322
if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0);
@@ -206,13 +334,20 @@ int kal_process_spawn_with(kal_dir base,
206334
okl::sys(okl::nr_dup3, granted[i], want, 0);
207335
}
208336

209-
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
210-
reinterpret_cast<okl_long>(args.slots),
211-
reinterpret_cast<okl_long>(envs.slots), 0);
337+
const okl_long why =
338+
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
339+
reinterpret_cast<okl_long>(args.slots),
340+
reinterpret_cast<okl_long>(envs.slots), 0);
341+
report.say(why);
212342
okl::sys(okl::nr_exit_group, 127);
213343
for (;;) { }
214344
}
215345

346+
if (const okl_long why = report.heard()) {
347+
reap(child);
348+
return okl::translate(why);
349+
}
350+
216351
*out = kal_process{ static_cast<kal_uintptr>(child) };
217352
return kal_ok;
218353
}

‎src/sys.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ enum : okl_long {
9191
nr_readv = 19, nr_writev = 20, nr_sched_yield = 24, nr_nanosleep = 35,
9292
nr_getpid = 39, nr_clone = 56, nr_execve = 59, nr_exit = 60, nr_wait4 = 61,
9393
nr_kill = 62, nr_ftruncate = 77, nr_getcwd = 79, nr_fsync = 74,
94+
nr_fcntl = 72,
9495
nr_arch_prctl = 158, nr_gettid = 186, nr_futex = 202,
9596
nr_getdents64 = 217, nr_set_tid_address = 218, nr_clock_gettime = 228,
9697
nr_clock_getres = 229, nr_exit_group = 231, nr_tgkill = 234,
@@ -178,7 +179,7 @@ enum : okl_long {
178179
nr_sched_yield = 124, nr_kill = 129, nr_tgkill = 131, nr_gettid = 178,
179180
nr_getpid = 172, nr_mmap = 222, nr_munmap = 215, nr_mprotect = 226,
180181
nr_clone = 220, nr_execve = 221, nr_wait4 = 260, nr_renameat = 38,
181-
nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1,
182+
nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1, nr_fcntl = 25,
182183
nr_arch_prctl = -1, nr_utimensat = 88, nr_symlinkat = 36, nr_fstatfs = 44,
183184
nr_getrandom = 278,
184185
// openkal.net and openkal.datagram
@@ -244,6 +245,13 @@ enum : okl_long {
244245
o_rdonly = 0, o_wronly = 1, o_rdwr = 2,
245246
o_creat = 0100, o_excl = 0200, o_trunc = 01000, o_append = 02000,
246247
o_cloexec = 02000000,
248+
249+
// ⭐ THE LOWEST FREE DESCRIPTOR AT OR ABOVE A BOUND, which is the one
250+
// primitive that moves a descriptor out of the way WITHOUT NAMING the
251+
// number it moves to --- and therefore without closing whatever a caller
252+
// already had there. `dup3' cannot do this: it is told the number, and it
253+
// closes what is on it.
254+
f_dupfd_cloexec = 1030,
247255
#if defined(__x86_64__)
248256
o_directory = 0200000, o_nofollow = 0400000,
249257
#else

0 commit comments

Comments
 (0)