-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathokm_spawn.c
More file actions
697 lines (653 loc) · 32.6 KB
/
Copy pathokm_spawn.c
File metadata and controls
697 lines (653 loc) · 32.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
/* Starting another program.
*
* The second of musl's own sources this port replaces, and the reason is the
* same shape as the first. musl starts a program by duplicating itself and
* then replacing the duplicate: `posix_spawn' calls clone with CLONE_VFORK and
* the duplicate calls execve. openkal has neither operation, and clause 7.1 of
* the specification records why: duplicating an address space and its
* execution state cannot be performed faithfully on every environment openkal
* targets, so an implementation obliged to reproduce it would be building a
* compatibility layer.
*
* What openkal has instead is the composite: kal_process_spawn starts a named
* program with an argument vector, a set of named values and a choice of
* streams. That is what `posix_spawn' means, so the replacement is a
* translation of arguments rather than a reconstruction of a mechanism --- and
* it is shorter than the code it replaces.
*
* What is lost is the part of `posix_spawn' that is expressed as instructions
* for the duplicate to perform: an arbitrary sequence of file actions. Four of
* the five are expressible in what openkal offers and are translated; the fifth
* is refused rather than ignored.
*
* --- what the started program's three streams are -------------------------
*
* ⚠️⚠️ THIS FILE USED TO PASS `{0, 0, 0}' AND LET THE FILE ACTIONS OVERWRITE IT,
* WHICH LOST EVERY REDIRECTION A CALLER HAD ALREADY PERFORMED.
*
* openkal spells "the stream its parent has" as a handle of zero (process.h),
* and an implementation reads that as the descriptor the CALLING IMAGE holds ---
* not the one this library's table holds. `dup2' rebinds this table and issues
* no system call, because there is none to issue: openkal has no operation that
* replaces one of a running program's own streams. So after
*
* dup2(fd, 1); posix_spawn(…) or dup2(fd, 1); execve(…)
*
* the started program wrote to the stream this program was STARTED with, and the
* file the caller had redirected onto stayed empty. Reported as
* openkal-linux#13; measured by a consumer, not here, because the probe in
* examples/subprocess started programs and never redirected first.
*
* ⭐ THE ATOM WAS ALREADY PRESENT. `kal_fs_stream' is a required operation of
* `openkal.fs' and this port has stored its result in every file description
* since okm_syscall.c opened one. What was missing was the route from the table
* to the spawn, which is the same shape as the defect that report began with.
*
* ⭐ AND ZERO IS STILL PASSED WHERE NOTHING WAS REDIRECTED, WHICH IS LOAD-BEARING.
* `KAL_PROCESS_PROP_STREAM_PASSING' exists because an environment may be able to
* start a program that inherits and unable to place a stream of the caller's
* choosing. Seeding with zero for an untouched descriptor asks for that capability
* only when a caller has actually asked for something, so a backend without it
* still runs every program that does not redirect.
*/
#define _GNU_SOURCE
#include "okm.h"
#include "okm_opt.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <spawn.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* musl's own description of a file action, included by path rather than
* copied: a copy would be a second definition of a structure this file reads
* and musl writes, and the two would agree only until one changed. */
#include "../../musl/src/process/fdop.h"
#undef malloc
#undef calloc
#undef realloc
#undef free
#define OKM_SPAWN_MAX 512
/* How many files one set of file actions may open. Eight rather than three: a
* caller may name one position more than once, and every file it opened has to
* be released whether or not its stream ended up being the one placed. */
#define OKM_SPAWN_MAX_OPEN 8
int __okm_child_record(struct kal_process h);
int __okm_child_record_job(struct kal_process h, struct kal_job j);
static size_t slen(const char* s) { size_t n = 0; while (s && s[n]) n++; return n; }
static int count(char* const* v) { int n = 0; while (v && v[n]) n++; return n; }
/* --- can this name be started at all? --------------------------------------
*
* ⚠️⚠️ ASKED HERE BECAUSE THE ANSWER DOES NOT COME BACK FROM BENEATH, AND A
* CALLER OF `execvp' CANNOT PROCEED WITHOUT IT.
*
* An implementation starts a program by duplicating itself and replacing the
* duplicate, and the replacement happens in the DUPLICATE --- so a name that
* cannot be started is discovered by a program that is no longer this one.
* openkal-linux ends that duplicate with 127 (`process.cpp'), and its
* `kal_process_spawn' returns `kal_ok' with a handle: the failure is reported
* to nobody.
*
* ⇒ `execve' then waited for the duplicate, read 127, and ENDED THE CALLING
* PROGRAM with it. musl's `execvp' issues one `execve' per PATH entry and
* relies on it RETURNING with errno set so it can try the next one, so the
* search could not survive its first miss: a program named without a slash was
* found only when it happened to sit in the first entry. Reported as
* openkal-linux#13 and measured by a consumer, who also measured that
* `bwrap' --- present at /usr/bin/bwrap --- was reported as not installed.
*
* ⭐ THE ENQUIRY IS ALREADY REQUIRED OF EVERY IMPLEMENTATION. `kal_fs_info' is
* an operation of `openkal.fs' and this file already resolves the name through
* `okm_resolve'; asking what the name refers to is one more call on a path that
* is about to start a program anyway.
*
* ⚠️ AND IT ANSWERS TWO OF THE THREE QUESTIONS, WHICH IS WHY A3 IS STILL OPEN.
* `ENOENT' and `ENOTDIR' are what a PATH search needs and are what this
* settles. Whether an existing file may be EXECUTED is not something openkal
* reports --- `kal_node_info' carries `writable' and no other permission --- so
* a name that exists and cannot be run still ends the caller with 127. That
* residue is recorded in README.md beside `access(X_OK)', which cannot answer
* it either and for the same reason. The complete answer needs the backend to
* report its own failure; asked for at openkal-linux. */
static int startable(struct kal_dir base, const char* rel)
{
struct kal_node_info info = { .self_size = sizeof info };
/* Resolves, because starting resolves. */
const int e = okm_fs_info(base, rel, slen(rel), 0, KAL_INFO_KIND, &info);
/* ⚠️ AN ENQUIRY THAT CANNOT BE MADE IS NOT AN ANSWER OF `NO'. A build
* configured without `openkal.fs' --- OKM_HAS_FS=0, which a machine with no
* storage is built with --- answers `not supported' here, and turning that
* into a refusal would stop a spawn this port would otherwise have
* attempted. The enquiry is an improvement on the failure that follows, not
* a precondition of it: where it cannot be made, the spawn answers as it
* did before this check existed. */
if (e == kal_err_not_supported) return 0;
if (e != kal_ok) return okm_errno(e);
if (info.kind == kal_node_absent) return ENOENT;
/* POSIX names this one: a directory is not a program, and `execve' upon one
* is EACCES rather than ENOENT. `execvp' continues its search on both. */
if (info.kind == kal_node_directory) return EACCES;
return 0;
}
/* The name to start, and whether it can be. On the one environment that spells
* a program with a suffix the name may be rewritten here, so that the enquiry
* and the spawn agree about which name they are talking about. */
static int startable_name(struct okm_at* at)
{
const int e = startable(at->base, at->rel);
#ifdef _WIN32
/* Tried second rather than first, so a file that genuinely bears the name
* is preferred to one that bears the name and the suffix --- the rule the
* spawn below already followed, moved up so that the enquiry follows it
* too. Without this the pre-check would refuse every name this environment
* would have found. */
if (e == ENOENT) {
const size_t n = slen(at->rel);
int has_suffix = 0;
for (size_t i = n; i > 0; i--) {
if (at->rel[i - 1] == '/') break;
if (at->rel[i - 1] == '.') { has_suffix = 1; break; }
}
if (!has_suffix && n + 4 < sizeof at->rel) {
at->rel[n + 0] = '.'; at->rel[n + 1] = 'e';
at->rel[n + 2] = 'x'; at->rel[n + 3] = 'e'; at->rel[n + 4] = 0;
if (startable(at->base, at->rel) == 0) return 0;
at->rel[n] = 0; /* put the name back */
}
}
#endif
return e;
}
/* --- what the started program's three streams are -------------------------- */
/* Places one stream, and answers with an error value where the interface has no
* way to say what the caller asked for.
*
* `placed' records whether anything the environment must be ABLE to place has
* been placed. A caller that duplicates descriptor 1 onto position 1 when
* descriptor 1 has not moved has asked for inheritance, so that case is
* normalised back to zero: asking a backend for a capability it does not need
* would refuse a spawn that can be performed.
*
* ⚠️⚠️ AND ZERO IS BOTH A SENTINEL AND A VALID HANDLE, WHICH IS NOT THIS FILE'S
* DOING AND IS THIS FILE'S PROBLEM.
*
* `kal_spawn_streams' spells inheritance as a handle of zero. `kal_stream' has
* no reserved value, and openkal-linux answers `kal_stdin()' with zero because
* its streams are the environment's own descriptors. The two are consistent for
* position zero --- placing standard input at standard input and inheriting it
* are the same act --- and inexpressible for the one case where they differ: a
* caller that redirects its OUTPUT onto its own standard input, which reaches
* here as the handle zero at position one.
*
* ⇒ Refused, rather than passed on. Passing it on would be read as inheritance
* and would start the program writing to the stream the caller had just
* redirected away from --- the silent wrong answer this whole change exists to
* remove. A refusal is one the caller can act upon. Recorded in
* musl/PATCHES.md, and reported upstream: the specification reserves a value in
* one interface that another interface may hand out. */
static int place(struct kal_spawn_streams* s, int pos, kal_uintptr stream,
int* placed)
{
if (stream == okm_std_stream(pos)) stream = 0;
else if (stream == 0) return ENOSYS;
else *placed = 1;
if (pos == 0) s->in.h = stream;
else if (pos == 1) s->out.h = stream;
else s->err.h = stream;
return 0;
}
/* The stream a descriptor carries, or a reason it carries none.
*
* ⚠️ THE ANSWER IS DECIDED BY THE KIND AND NOT BY THE VALUE, and that distinction
* is the whole of it: a stream handle of zero is a perfectly ordinary handle on
* an implementation whose streams are its own descriptors, so "the handle is
* zero" cannot be read as "there is no stream". A directory has no stream
* because it is a directory; an unconnected socket has none because openkal
* produces a connection and a stream together, which is the one kind for which
* zero does mean absence and okm_net.c is where that convention is set.
*
* The classification is the one okm_syscall.c's `stream_of' makes for reading
* and writing, and the two agree deliberately: a descriptor a program can write
* to is a descriptor a started program can be given. */
static int stream_for_spawn(int fd, kal_uintptr* out)
{
struct okm_desc* d = okm_desc_of(fd);
if (!d) return EBADF;
switch (d->kind) {
case OKM_STREAM:
case OKM_CHANNEL:
case OKM_FILE:
*out = d->stream;
return 0;
case OKM_SOCKET:
if (d->stream == 0) return ENOTCONN;
*out = d->stream;
return 0;
default:
return EBADF; /* a directory, or a descriptor that is not open */
}
}
/* The three positions before any file action is applied.
*
* ⭐ THIS IS THE WHOLE OF THE FIX FOR openkal-linux#13, AND IT IS THREE LINES OF
* DECISION rather than a mechanism: a descriptor that still names what it named
* when the program began is inheritance and is spelled zero; one that names
* something else is a redirection the caller performed and is carried across;
* one that names nothing is refused. */
static int seed(struct kal_spawn_streams* s, int* placed)
{
for (int fd = 0; fd < 3; fd++) {
kal_uintptr stream = 0;
int e = stream_for_spawn(fd, &stream);
if (!e) e = place(s, fd, stream, placed);
if (e) return e;
}
return 0;
}
/* ⭐⭐ WHETHER THE STARTED PROGRAM MAY OUTLIVE THIS ONE, WHICH IS THE THING
* `execve' MEANS AND NOTHING ELSE HERE DOES.
*
* `execve' is composed as starting a program and ending with its status, so
* there are three images where a system with the operation has two: this one,
* the copy that waits, and the program. A signal aimed at the identifier the
* caller holds reaches the WAITER --- measured with a host as control: identical
* status words, opposite outcomes, the caller told the program died while the
* program ran to completion, unsupervised. openkal-linux#13.
*
* openkal 0.10 adds `kal_process_spawn_bound', which says the thing that could
* not be said. It is asked for ONLY where `execve' is meant, because an ordinary
* `posix_spawn' means the opposite: POSIX children outlive their parents.
*
* ⚠️ AND A BACKEND MAY DECLINE IT, in which case this falls back to the
* unbound spawn rather than refusing to start the program at all. A caller of
* `execve' that gets an unbound program is where this port has always been; a
* caller that gets no program is worse. The difference is recorded in
* musl/PATCHES.md and is what `KAL_PROCESS_PROP_BOUND_LIFETIME' is for. */
static int start_program(int bound, struct kal_job* unit, struct kal_dir work,
struct okm_at* at,
const char** a_ptr, const kal_uintptr* a_len, int argc,
const char** e_ptr, const kal_uintptr* e_len, int envc,
struct kal_spawn_streams* streams,
struct kal_process* child)
{
const kal_uintptr props = okm_process_props();
struct kal_spawn how;
how.base = at->base;
/* ⭐ THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS THIS LIBRARY'S OWN AND NOT
* `base'. `base' is whichever preopen the program's NAME resolved under ---
* for `/usr/bin/sh' that is the root --- and before openkal 0.11 it was the
* only directory a spawn carried, so a started program ran wherever the
* implementation happened to be. `chdir' moved what THIS library resolves
* names against and nothing else, so a caller that chdir'd and then started a
* program was the one who found out. */
how.work = work;
/* ⭐ THE UNIT, WHICH IS WHAT `POSIX_SPAWN_SETPGROUP' MEANS HERE. A caller
* that asked for it gets a `kal_job' whose identity the start establishes;
* a backend that does not claim the position is given no unit at all rather
* than a refusal, for the same reason the binding below is optional. */
how.job = (unit && (props & KAL_PROCESS_PROP_JOB)) ? unit : 0;
how.grants = 0;
how.grant_count = 0;
how.flags = 0;
/* ⚠️ ASKED FOR ONLY WHERE IT IS MEANT, AND ONLY WHERE IT IS ANSWERED.
*
* `execve' is composed as starting a program and ending with its status, so
* the binding is what makes the composition behave like the operation. An
* ordinary `posix_spawn' means the opposite --- POSIX children outlive their
* parents --- so `bound' is false there.
*
* A backend that does not claim a position gets the request WITHOUT it
* rather than a refusal: a caller of `execve' that gets an unbound program is
* where this port has always been, and a caller that gets no program at all
* is worse. The same reasoning covers the job. */
if (bound && (props & KAL_PROCESS_PROP_BOUND_LIFETIME))
how.flags |= KAL_SPAWN_BOUND_LIFETIME;
return okm_process_spawn(&how, at->rel, slen(at->rel),
a_ptr, a_len, (kal_uintptr)argc,
e_ptr, e_len, (kal_uintptr)envc,
streams, child);
}
int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
const posix_spawn_file_actions_t* fa,
const posix_spawnattr_t* restrict attr,
char* const argv[restrict], char* const envp[restrict],
int bound);
int __posix_spawn(pid_t* restrict res, const char* restrict path,
const posix_spawn_file_actions_t* fa,
const posix_spawnattr_t* restrict attr,
char* const argv[restrict], char* const envp[restrict])
{
return __okm_spawn_common(res, path, fa, attr, argv, envp, 0);
}
int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
const posix_spawn_file_actions_t* fa,
const posix_spawnattr_t* restrict attr,
char* const argv[restrict], char* const envp[restrict],
int bound)
{
if (!res || !path) return EINVAL;
/* ⭐ `POSIX_SPAWN_SETPGROUP' IS HONOURED SINCE 0.12, AND ONLY IN THE ONE FORM
* THIS PORT CAN MEAN.
*
* The attribute carries a group to join, and a caller that asks to join
* SOMEBODY ELSE'S group is asking for a thing openkal has no way to say ---
* `KAL_SPAWN_OWN_JOB' makes a program the start of its own unit and cannot
* put it into an existing one. So a zero group, which is the spelling for
* "a group of your own", is answered; a named group is still refused rather
* than silently turned into a different one.
*
* ⚠️ That is exactly the case the consumer writes: `setpgid(0, 0)' in the
* child so that a timeout can kill the whole tree. */
if (attr && (attr->__flags & ~(POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK
| POSIX_SPAWN_SETPGROUP)))
return ENOSYS;
if (attr && (attr->__flags & POSIX_SPAWN_SETPGROUP) && attr->__pgrp != 0)
return ENOSYS;
/* ⚠️ musl carries the PATH SEARCH in this field: `posix_spawnp' stores
* `__execvpe' there and its `posix_spawn' calls it in the duplicate instead
* of `execve'. This file replaces that duplicate, so the field was read by
* nobody --- and `posix_spawnp("sh", …)' therefore started `./sh', failed,
* and REPORTED SUCCESS. The search is performed by `__posix_spawnp' below
* and never arrives here, so a function found in this field now is one this
* file does not know how to honour, and is refused rather than ignored. */
if (attr && attr->__fn) return ENOSYS;
struct okm_at at;
{
const long r = okm_resolve(AT_FDCWD, path, &at, 0);
if (r) return (int)-r;
}
/* Before anything is locked or opened, because a name that cannot be
* started needs no cleanup and a caller searching a PATH needs the answer
* more than it needs anything else this function does. */
{
const int e = startable_name(&at);
if (e) return e;
}
const int argc = count(argv);
const int envc = count(envp);
if (argc >= OKM_SPAWN_MAX || envc >= OKM_SPAWN_MAX) return E2BIG;
static const char* a_ptr[OKM_SPAWN_MAX];
static kal_uintptr a_len[OKM_SPAWN_MAX];
static const char* e_ptr[OKM_SPAWN_MAX];
static kal_uintptr e_len[OKM_SPAWN_MAX];
okm_lock();
for (int i = 0; i < argc; i++) { a_ptr[i] = argv[i]; a_len[i] = slen(argv[i]); }
for (int i = 0; i < envc; i++) { e_ptr[i] = envp[i]; e_len[i] = slen(envp[i]); }
/* The streams the started program receives. Zero means it inherits the
* corresponding stream of its parent, which is what an environment with no
* general mechanism for passing handles can always provide. */
struct kal_spawn_streams streams = { { 0 }, { 0 }, { 0 } };
int placed = 0; /* a stream of the caller's choosing */
int refused = seed(&streams, &placed);
/* Files a file action opened. They belong to this call rather than to the
* caller --- the caller asked for a program started upon them, not for a
* file --- so each is released once the spawn has been performed.
*
* ⚠️ THE BOUND IS NOT THREE. Three positions can be placed, and a caller may
* name one of them more than once; POSIX says the last such action decides,
* and the earlier file is still open and still has to be released. The bound
* is stated rather than derived, and a sequence that exceeds it is refused
* rather than silently truncated. */
struct kal_file opened[OKM_SPAWN_MAX_OPEN];
int opened_n = 0;
/* The directory the program is to run in, if a file action named one. It is
* this call's and is released with the opened files below. */
struct kal_dir where;
int where_held = 0;
/* Resolving a file action's name needs one of these and it is four kilobytes.
* Static, under this file's lock, for the same reason the argument vectors
* above are: a second one on the stack would double the frame of a function
* that a program with a small thread stack reaches. */
static struct okm_at fa_at;
if (!refused && fa) {
/* ⚠️⚠️ IN THE ORDER THEY WERE ADDED, WHICH IS NOT THE ORDER THE LIST IS
* IN. musl's `posix_spawn_file_actions_add*' PREPEND, so `__actions'
* names the most recent one; musl's own `posix_spawn' walks to the tail
* and then follows `prev'. This file followed `next' and therefore
* applied a caller's actions in reverse.
*
* Invisible until two actions name one position: `popen' emits a single
* `adddup2' and was correct throughout. A caller that opened a file onto
* position 1 and then duplicated something else onto it would have got
* the file. */
struct fdop* op = fa->__actions;
if (op) while (op->next) op = op->next;
for (; op; op = op->prev) {
switch (op->cmd) {
case FDOP_DUP2: {
if (op->fd > 2 || op->fd < 0) { refused = ENOSYS; break; }
kal_uintptr s = 0;
refused = stream_for_spawn(op->srcfd, &s);
if (refused) break;
refused = place(&streams, op->fd, s, &placed);
break;
}
case FDOP_OPEN: {
/* ⭐ EXPRESSIBLE, AND IT WAS REFUSED. `kal_fs_open' produces the
* file and `kal_fs_stream' produces the stream to place, both of
* them required operations this port already calls elsewhere.
* Refusing it forced every caller wanting a program's output in
* a file through `dup2' --- which is the route openkal-linux#13
* reported as not working. */
if (op->fd > 2 || op->fd < 0) { refused = ENOSYS; break; }
if (opened_n >= OKM_SPAWN_MAX_OPEN) { refused = ENOSYS; break; }
const long r = okm_resolve(AT_FDCWD, op->path, &fa_at, 0);
if (r) { refused = (int)-r; break; }
struct kal_file f;
/* ⚠️ `op->mode' is not among the inputs, exactly as it is not for
* `open'. README.md's divergence table records what a program
* observes; a mode accepted here and dropped beneath would be
* this file reporting success having done something else. */
const int oe = okm_fs_open(fa_at.base, fa_at.rel, slen(fa_at.rel),
okm_open_flags(op->oflag), &f);
if (oe != kal_ok) { refused = okm_errno(oe); break; }
opened[opened_n++] = f;
refused = place(&streams, op->fd, okm_fs_stream(f).h, &placed);
break;
}
case FDOP_CLOSE:
/* ⭐ TWO ANSWERS, DECIDED BY WHICH DESCRIPTOR IS NAMED. The
* comment this replaces gave one of them for both, on the ground
* that "nothing is inherited that was not asked for" --- which is
* true above position two and false at or below it, because the
* three positions are inherited by construction.
*
* ⚠️ So closing one of the three is an action that cannot be
* performed: openkal has no value meaning "no stream", and the
* value that looks like one means the opposite. Accepting it and
* doing nothing would hand a program the standard input its
* caller had just taken away, which is the failure a caller
* cannot see and cannot act upon. */
if (op->fd > 2) break;
refused = ENOSYS;
break;
/* ⭐⭐ ANSWERED SINCE 0.12, BECAUSE openkal 0.11 GAVE A SPAWN A SECOND
* DIRECTORY. Both of these say the same thing --- run the program
* HERE --- and until there was a place to put it they were refused
* along with everything else this file could not express. */
case FDOP_CHDIR: {
struct okm_at cat;
const int r = okm_resolve(AT_FDCWD, op->path, &cat, 0);
if (r) { refused = (int)-r; break; }
struct kal_dir d;
const int e = okm_fs_open_dir(cat.base, cat.rel, slen(cat.rel), &d);
if (e != kal_ok) { refused = okm_errno(e); break; }
if (where_held) okm_fs_close_dir(where);
where = d; where_held = 1;
break;
}
case FDOP_FCHDIR: {
struct okm_desc* dd = okm_desc_of(op->fd);
if (!dd || dd->kind != OKM_DIR) { refused = EBADF; break; }
/* A directory of this program's own, opened again so that the
* spawn holds one the caller cannot close underneath it. */
struct kal_dir d;
const int e = okm_fs_open_dir(dd->dir, ".", 1, &d);
if (e != kal_ok) { refused = okm_errno(e); break; }
if (where_held) okm_fs_close_dir(where);
where = d; where_held = 1;
break;
}
default:
/* An action openkal cannot express. Performing the spawn
* without it would start the program in a state the caller did
* not ask for, which is worse than not starting it. */
refused = ENOSYS;
break;
}
if (refused) break;
}
}
/* ⭐ THE CAPABILITY IS REQUIRED ONLY WHERE IT IS USED.
*
* `KAL_PROCESS_PROP_STREAM_PASSING' exists because an environment may be
* able to start a program that inherits and unable to give it a stream of
* the caller's choosing. Every position that was left as inheritance is
* zero, so a program that redirects nothing asks for nothing, and only a
* caller that did redirect meets the refusal --- which is the answer it can
* act upon rather than a program started somewhere it did not ask for. */
if (!refused && placed
&& !(okm_process_props() & KAL_PROCESS_PROP_STREAM_PASSING))
refused = ENOSYS;
if (refused) {
for (int i = 0; i < opened_n; i++) okm_fs_close_file(opened[i]);
if (where_held) okm_fs_close_dir(where);
okm_unlock();
return refused;
}
/* The vector is passed unaltered, argv[0] included. Clause 7.6: the
* started program reads its own name through kal_env_arg(0), so a caller
* that did not supply it could not predict what the program would read. */
struct kal_process child;
/* ⚠️ THE UNIT IS THIS CALL'S, AND IT IS NOT KEPT ANYWHERE AFTERWARDS --- see
* the note at the end of this function. */
struct kal_job unit = { 0 };
struct kal_job* want_unit =
(attr && (attr->__flags & POSIX_SPAWN_SETPGROUP)) ? &unit : 0;
/* ⭐ THE WORKING DIRECTORY IS THIS LIBRARY'S, AND THAT IS THE WHOLE FIX.
*
* `chdir' here moves `okm_cwd_dir' and nothing else, because openkal has no
* operation that moves a running program's. Before 0.11 a spawn carried one
* directory --- the one the program's NAME resolved under --- so a caller that
* chdir'd and then started a program found the program running somewhere
* else entirely. Passing it now closes that, and it closes the `fork' route
* as well: a copy that chdir'd has its own `okm_cwd_dir', and the `execve'
* it then performs reaches this line. */
int e = start_program(bound, want_unit, where_held ? where : okm_cwd_dir,
&at, a_ptr, a_len, argc,
e_ptr, e_len, envc, &streams, &child);
/* ⭐ THE ONE ENVIRONMENT THAT SPELLS A PROGRAM WITH A SUFFIX IS ANSWERED
* BEFORE THIS POINT AND NOT AFTER IT.
*
* A program there is a file whose name ends in a particular suffix, and a
* caller of this interface names programs without one; the difference is
* resolved here rather than beneath, because openkal is deliberately
* literal about names and does not know that a program is a kind of file.
*
* ⚠️ It used to be resolved by RETRYING the spawn on `kal_err_not_found'.
* That cannot stay: the enquiry added above refuses an absent name before
* the spawn is reached, so the retry would never run and every suffixless
* name on that environment would be refused. `startable_name' therefore
* owns the choice, keeps the same order --- the bare name first --- and
* rewrites `at.rel' so that what was asked about is what is started. */
/* ⭐ THE FILES A FILE ACTION OPENED ARE RELEASED HERE, AND THE STARTED
* PROGRAM KEEPS ITS STREAM.
*
* That the second survives the first is what `kal_process_channel' already
* requires of an implementation in as many words --- "a parent that does not
* release `theirs' after the spawn never observes the end of input on
* `mine'" --- so a spawn that did not carry the stream across would make that
* instruction impossible to follow. The same holds for a stream that came
* from a file, and openkal-musl asks the specification to say so for streams
* in general rather than for one of them. */
for (int i = 0; i < opened_n; i++) okm_fs_close_file(opened[i]);
if (where_held) okm_fs_close_dir(where);
okm_unlock();
if (e != kal_ok) return okm_errno(e);
const int pid = __okm_child_record_job(child, unit);
if (pid < 0) { okm_process_close(child); return EAGAIN; }
*res = (pid_t)pid;
return 0;
}
weak_alias(__posix_spawn, posix_spawn);
/* --- starting a program named without a path -------------------------------
*
* The third of musl's own sources this port replaces, and it is replaced
* because the second one was.
*
* musl does not search a PATH here. It stores `__execvpe' in the attributes and
* lets its `posix_spawn' call that IN THE DUPLICATE instead of `execve', so the
* search happens inside a program that has already been started. This port has
* no duplicate to run it in --- `__posix_spawn' translates a spawn into
* `kal_process_spawn' --- so the field was read by nobody and the name was
* taken as a path relative to the working directory. `posix_spawnp("sh", …)'
* started `./sh', which is not there, and reported SUCCESS.
*
* ⇒ The search is performed here, one `__posix_spawn' per entry, which is the
* arrangement musl's own `__execvpe' uses one `execve' per entry for. It works
* for the same reason that one now works: `__posix_spawn' reports a name it
* cannot start rather than starting something that ends with 127.
*
* The rules are musl's, so that a program moved onto this port meets the answer
* it met before: a name containing a separator is not searched for; an empty
* entry means the working directory; `EACCES' anywhere is remembered and
* reported only if nothing is found; and any other error ends the search at
* once, because it is not evidence about the next entry.
*
* ⚠️ INCLUDING THE ENTRY SEPARATOR, WHICH IS A COLON ON EVERY TARGET AND IS THE
* WRONG ONE FOR EXACTLY ONE OF THEM.
*
* One environment separates its own PATH with a semicolon and begins each entry
* with a volume letter and a colon, so a colon-separated reading of its PATH
* produces entries that are not names. The reason it is still a colon here is
* that `execvp' --- which this port does NOT replace, and which reaches
* `execve' --- splits on a colon in musl's own source. Splitting differently in
* this function would make the two ways of searching for one program disagree
* with each other, which is a worse thing for a caller to meet than one that is
* wrong in a way both share. Recorded in musl/PATCHES.md; the CI row for that
* environment declares no shell, so nothing there searches a PATH today. */
int __posix_spawnp(pid_t* restrict res, const char* restrict file,
const posix_spawn_file_actions_t* fa,
const posix_spawnattr_t* restrict attr,
char* const argv[restrict], char* const envp[restrict])
{
if (!res || !file) return EINVAL;
if (!*file) return ENOENT;
for (const char* s = file; *s; s++)
if (*s == '/') return __posix_spawn(res, file, fa, attr, argv, envp);
const char* path = getenv("PATH");
/* musl's own default, and it is here for the same reason it is there: a
* program that never set PATH still has somewhere to be looked for. */
if (!path) path = "/usr/local/bin:/bin:/usr/bin";
const size_t k = slen(file);
if (k > NAME_MAX) return ENAMETOOLONG;
/* ⚠️ ON THE STACK, WHICH IS WHAT musl DOES TOO (a variable-length array of
* the same bound). The static buffers above are under this file's lock and
* this function runs before it is taken --- a static here would be a race
* between two contexts searching at once, which is worse than a frame. */
char cand[OKM_MAX_PATH];
int seen_eacces = 0;
for (const char* p = path; ; ) {
const char* z = p;
while (*z && *z != ':') z++;
const size_t n = (size_t)(z - p);
/* An entry that cannot be joined to the name is skipped rather than
* truncated: a truncated name is a different name and might exist. */
if (n + (n ? 1 : 0) + k + 1 <= sizeof cand) {
size_t o = 0;
for (size_t i = 0; i < n; i++) cand[o++] = p[i];
if (o) cand[o++] = '/';
for (size_t i = 0; i <= k; i++) cand[o + i] = file[i];
const int e = __posix_spawn(res, cand, fa, attr, argv, envp);
if (!e) return 0;
if (e == EACCES) seen_eacces = 1;
else if (e != ENOENT && e != ENOTDIR) return e;
}
if (!*z) break;
p = z + 1;
}
return seen_eacces ? EACCES : ENOENT;
}
weak_alias(__posix_spawnp, posix_spawnp);