Skip to content

Commit a891730

Browse files
authored
0.12.0 --- the working directory reaches the program, and a unit can be asked for (#26)
* 0.12.0 --- the working directory reaches the program, and a unit can be asked for Two of the three things a consumer's own tests were red on are now green, and both were measured against their tree rather than against a probe of mine. ⭐ `chdir' NOW REACHES A STARTED PROGRAM. openkal 0.11 carries a second directory per start, so `okm_cwd_dir' --- which is what `chdir' has always moved, this library having no other place to move --- is passed as the one the program runs in. That closes the `fork' route too: a copy that chdir'd has its own `okm_cwd_dir', and the `execve' it then performs reaches the same line. `POSIX_SPAWN_SETPGROUP' is honoured, in the one form this port can mean: a zero group asks for a unit of the program's own, which openkal 0.11's `kal_spawn.job' says exactly. A NAMED group is still refused rather than quietly turned into a different one --- openkal cannot put a program into somebody else's unit. ⚠️ WHAT IS STILL RED, AND IT IS ONE ASSERTION. The consumer forms its group with `fork'; `setpgid(0, 0)'; `execvp' and then kills by number from the parent. The unit that start forms belongs to the STARTED program, whose identifier is not the copy's --- so the parent's `kill(-pid)' names a group that is not the one that exists. Closing it needs an operation that puts THE CALLING program into a unit, which openkal does not have and which is the obvious next declaration. Recorded rather than approximated. Measured on the consumer's tree, both architectures: `cwd: 子进程 chdir 生效' and `EOF 早到: 进程未残留' green, `超时: 后台后代随组死' still red; agent-core 42/9 to 43/8, with `test_chat_tools' bash-cwd now green. * setpgid forms a real unit, kill(-n) reaches it, and a spawn can chdir Three calls a shell runner makes, each of which used to succeed and change nothing a caller could observe --- which is the failure shape this port names in okm_opt.h and had three more instances of. `setpgid(0, 0)' answered 0 and formed nothing. True in a world with no groups, so it was not a lie; it was also not a unit, and the caller's next act --- `kill(-pid)' --- found nothing to kill. It is now `kal_process_job_enter'. `kill(-n)' answered ESRCH while a unit existed. It now names the unit: the one this program formed, or the one a start formed for a child. ⚠️ THE UNIT IS KEPT PAST THE WAIT, and that is the case a unit is used FOR --- a shell exits at once and the work it backgrounded is what a timeout has to reach. Clearing it on the wait made this answer ESRCH a fraction of a second before every caller that wants it; measured, the background work survived. `posix_spawn_file_actions_addchdir_np' was refused along with everything this file could not express. openkal 0.11 gives a spawn a second directory, so both chdir actions are now answered. ⚠️ `POSIX_SPAWN_SETPGROUP' is honoured only in the form this port can mean: a zero group asks for a unit of the program's own, which is exactly `kal_spawn.job'. A NAMED group stays refused --- openkal cannot put a program into somebody else's unit, and turning that request into a different one quietly is the defect above with a fourth instance. ⚠️⚠️ AND ONE IDIOM REMAINS OUT OF REACH, WHICH IS RECORDED AND NOT APPROXIMATED. `fork(); setpgid(0, 0); exec…' forms a unit led by the program the COPY starts, whose identity the original never learns --- so the original's `kill(-pid)' names a group that is not the one that exists. Closing it needs the copy and its parent to agree on a name before either exists, which nothing here can arrange. `posix_spawn' with the attribute above is the form that works, and it is what the consumer moved to. * The execve waiter lets go of every stream it holds ⚠️⚠️ A replacement leaves ONE image; this composition leaves two, and the one that remains still held every file description the caller had --- including the write end of a pipe it had just placed at the started program's standard output. A pipe reports the end of input when the LAST writer lets go, so the reader on the other side saw a stream that was still open, from a program that had ended. ⭐ Measured through a consumer: an MCP server that exits mid-request should be reported as "Connection closed" and was reported as "Timed out after 1000ms". The server was gone, nobody was writing, and the pipe stayed open because of a waiter neither side knew existed. ⚠️ THIS IS THE 0.10 DEFECT'S THIRD FACE. `kal_process_spawn_bound' was added because a SIGNAL reached the middle image; this is the middle image holding a RESOURCE. Same fact, same fix: make that image as invisible as it claims to be.
1 parent 0e2c085 commit a891730

6 files changed

Lines changed: 286 additions & 35 deletions

File tree

mcpp.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-musl"
4-
version = "0.11.0"
4+
version = "0.12.0"
55
description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel."
66
license = "Apache-2.0"
77

@@ -15,7 +15,7 @@ authors = ["mcpplibs"]
1515
repo = "https://github.com/mcpplibs/openkal-musl"
1616

1717
[dependencies]
18-
openkal = "0.10.0"
18+
openkal = "0.11.0"
1919

2020
# An ordinary consumer of openkal declares the specification and leaves the
2121
# choice of implementation to whoever builds the program, which is what the
@@ -30,10 +30,10 @@ openkal = "0.10.0"
3030
#
3131
# The consequence for a program is that it names this package and nothing else.
3232
[target.'cfg(os = "linux")'.dependencies]
33-
openkal-linux = { version = "0.9.0", features = ["standalone"] }
33+
openkal-linux = { version = "0.10.0", features = ["standalone"] }
3434

3535
[target.'cfg(os = "macos")'.dependencies]
36-
openkal-macos = { version = "0.7.0", features = ["standalone"] }
36+
openkal-macos = { version = "0.8.0", features = ["standalone"] }
3737

3838
# ⚠️ FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT.
3939
#
@@ -44,7 +44,7 @@ openkal-macos = { version = "0.7.0", features = ["standalone"] }
4444
# runtime that receives control, and a C library configured for an environment
4545
# with no process to exit from. So this declares the implementation and stops.
4646
[target.'cfg(os = "none")'.dependencies]
47-
openkal-opensbi = { version = "0.4.0", features = ["standalone"] }
47+
openkal-opensbi = { version = "0.5.0", features = ["standalone"] }
4848

4949
# ⭐ WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH IS EXPECTED TO PROVIDE.
5050
#
@@ -69,7 +69,7 @@ openkal-opensbi = { version = "0.4.0", features = ["standalone"] }
6969
defines = ["OKM_HAS_FS=0", "OKM_HAS_PROCESS=0", "OKM_HAS_TASK=0"]
7070

7171
[target.'cfg(windows)'.dependencies]
72-
openkal-windows = { version = "0.5.0", features = ["standalone"] }
72+
openkal-windows = { version = "0.6.0", features = ["standalone"] }
7373

7474
# The feature macros musl's own build establishes.
7575
#

port/src/okm.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ void okm_unlock(void);
161161
/* The table. */
162162
int okm_fd_alloc(int from); /* the lowest free descriptor */
163163
void okm_fd_release(int fd);
164+
165+
/* Every stream this image holds, released. The one caller is the `execve'
166+
* composition, whose waiting image must not keep a pipe open on behalf of a
167+
* program that has already ended --- see the definition. */
168+
void __okm_close_all_for_exec(void);
164169
struct okm_desc* okm_desc_of(int fd);
165170
int okm_fd_bind(int fd, int kind, kal_uintptr stream,
166171
struct kal_file file, struct kal_dir dir, int flags);

port/src/okm_fd.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,25 @@ int okm_absolute(int dirfd, const char* path, char* out, size_t cap)
521521
if (d->path_slot < 0 || !g_dirpath[d->path_slot][0]) return -ENOENT;
522522
return join(g_dirpath[d->path_slot], path, out, cap);
523523
}
524+
525+
/* Every stream this image holds, released --- used by the one place that has an
526+
* image with nothing left to do.
527+
*
528+
* ⚠️⚠️ IT EXISTS BECAUSE `execve' HERE LEAVES AN IMAGE BEHIND. A replacement
529+
* leaves one image; this library composes it as start, wait, end, which leaves
530+
* two --- and the second still holds every file description the caller had. A pipe
531+
* reports the end of input when the LAST writer lets go, so a waiter holding the
532+
* write end kept a reader on the other side waiting for a program that had
533+
* already ended. Measured through a consumer: a server that exits mid-request was
534+
* reported as a timeout rather than as a closed connection.
535+
*
536+
* ⚠️ NOT A GENERAL `close everything'. The standard streams are released too,
537+
* which is right HERE and wrong anywhere else: this image writes nothing more.
538+
* The one caller is the `execve' composition, immediately before it waits. */
539+
void __okm_close_all_for_exec(void)
540+
{
541+
okm_lock();
542+
for (int fd = 0; fd < OKM_MAX_FD; fd++)
543+
if (g_fd[fd].desc >= 0) okm_fd_release(fd);
544+
okm_unlock();
545+
}

port/src/okm_opt.h

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ static inline int okm_fs_list_next(struct kal_dir, kal_uintptr*, char*, kal_uint
170170
#if OKM_HAS_PROCESS
171171

172172
#define okm_process_spawn kal_process_spawn
173-
#define okm_process_spawn_bound kal_process_spawn_bound
173+
#define okm_process_job_enter kal_process_job_enter
174+
#define okm_process_job_terminate kal_process_job_terminate
174175
#define okm_process_wait kal_process_wait
175176
#define okm_process_terminate kal_process_terminate
176177
#define okm_process_close kal_process_close
@@ -201,19 +202,16 @@ static inline kal_uintptr okm_process_props(void)
201202
/* Nothing is provided, so nothing is claimed. */
202203
static inline kal_uintptr okm_process_props(void) { return 0; }
203204

204-
static inline int okm_process_spawn(struct kal_dir, const char*, kal_uintptr,
205-
const char**, const kal_uintptr*, kal_uintptr,
206-
const char**, const kal_uintptr*, kal_uintptr,
207-
const struct kal_spawn_streams*,
208-
struct kal_process*) { return kal_err_not_supported; }
209-
static inline int okm_process_spawn_bound(struct kal_dir, const char*, kal_uintptr,
205+
static inline int okm_process_spawn(const struct kal_spawn*, const char*, kal_uintptr,
210206
const char**, const kal_uintptr*, kal_uintptr,
211207
const char**, const kal_uintptr*, kal_uintptr,
212208
const struct kal_spawn_streams*,
213209
struct kal_process*) { return kal_err_not_supported; }
214210
static inline int okm_process_wait(struct kal_process, int*,
215211
int*) { return kal_err_not_supported; }
216212
static inline int okm_process_terminate(struct kal_process) { return kal_err_not_supported; }
213+
static inline int okm_process_job_enter(struct kal_job*) { return kal_err_not_supported; }
214+
static inline int okm_process_job_terminate(struct kal_job) { return kal_err_not_supported; }
217215
static inline void okm_process_close(struct kal_process) {}
218216

219217
#endif /* OKM_HAS_PROCESS */

port/src/okm_spawn.c

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
#define OKM_SPAWN_MAX_OPEN 8
7979

8080
int __okm_child_record(struct kal_process h);
81+
int __okm_child_record_job(struct kal_process h, struct kal_job j);
8182

8283
static size_t slen(const char* s) { size_t n = 0; while (s && s[n]) n++; return n; }
8384

@@ -276,20 +277,49 @@ static int seed(struct kal_spawn_streams* s, int* placed)
276277
* `execve' that gets an unbound program is where this port has always been; a
277278
* caller that gets no program is worse. The difference is recorded in
278279
* musl/PATCHES.md and is what `KAL_PROCESS_PROP_BOUND_LIFETIME' is for. */
279-
static int start_program(int bound, struct okm_at* at,
280+
static int start_program(int bound, struct kal_job* unit, struct kal_dir work,
281+
struct okm_at* at,
280282
const char** a_ptr, const kal_uintptr* a_len, int argc,
281283
const char** e_ptr, const kal_uintptr* e_len, int envc,
282284
struct kal_spawn_streams* streams,
283285
struct kal_process* child)
284286
{
285-
if (bound && (okm_process_props() & KAL_PROCESS_PROP_BOUND_LIFETIME)) {
286-
const int e = okm_process_spawn_bound(at->base, at->rel, slen(at->rel),
287-
a_ptr, a_len, (kal_uintptr)argc,
288-
e_ptr, e_len, (kal_uintptr)envc,
289-
streams, child);
290-
if (e != kal_err_not_supported) return e;
291-
}
292-
return okm_process_spawn(at->base, at->rel, slen(at->rel),
287+
const kal_uintptr props = okm_process_props();
288+
289+
struct kal_spawn how;
290+
how.base = at->base;
291+
/* ⭐ THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS THIS LIBRARY'S OWN AND NOT
292+
* `base'. `base' is whichever preopen the program's NAME resolved under ---
293+
* for `/usr/bin/sh' that is the root --- and before openkal 0.11 it was the
294+
* only directory a spawn carried, so a started program ran wherever the
295+
* implementation happened to be. `chdir' moved what THIS library resolves
296+
* names against and nothing else, so a caller that chdir'd and then started a
297+
* program was the one who found out. */
298+
how.work = work;
299+
/* ⭐ THE UNIT, WHICH IS WHAT `POSIX_SPAWN_SETPGROUP' MEANS HERE. A caller
300+
* that asked for it gets a `kal_job' whose identity the start establishes;
301+
* a backend that does not claim the position is given no unit at all rather
302+
* than a refusal, for the same reason the binding below is optional. */
303+
how.job = (unit && (props & KAL_PROCESS_PROP_JOB)) ? unit : 0;
304+
how.grants = 0;
305+
how.grant_count = 0;
306+
how.flags = 0;
307+
308+
/* ⚠️ ASKED FOR ONLY WHERE IT IS MEANT, AND ONLY WHERE IT IS ANSWERED.
309+
*
310+
* `execve' is composed as starting a program and ending with its status, so
311+
* the binding is what makes the composition behave like the operation. An
312+
* ordinary `posix_spawn' means the opposite --- POSIX children outlive their
313+
* parents --- so `bound' is false there.
314+
*
315+
* A backend that does not claim a position gets the request WITHOUT it
316+
* rather than a refusal: a caller of `execve' that gets an unbound program is
317+
* where this port has always been, and a caller that gets no program at all
318+
* is worse. The same reasoning covers the job. */
319+
if (bound && (props & KAL_PROCESS_PROP_BOUND_LIFETIME))
320+
how.flags |= KAL_SPAWN_BOUND_LIFETIME;
321+
322+
return okm_process_spawn(&how, at->rel, slen(at->rel),
293323
a_ptr, a_len, (kal_uintptr)argc,
294324
e_ptr, e_len, (kal_uintptr)envc,
295325
streams, child);
@@ -316,7 +346,22 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
316346
int bound)
317347
{
318348
if (!res || !path) return EINVAL;
319-
if (attr && (attr->__flags & ~(POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK)))
349+
/* ⭐ `POSIX_SPAWN_SETPGROUP' IS HONOURED SINCE 0.12, AND ONLY IN THE ONE FORM
350+
* THIS PORT CAN MEAN.
351+
*
352+
* The attribute carries a group to join, and a caller that asks to join
353+
* SOMEBODY ELSE'S group is asking for a thing openkal has no way to say ---
354+
* `KAL_SPAWN_OWN_JOB' makes a program the start of its own unit and cannot
355+
* put it into an existing one. So a zero group, which is the spelling for
356+
* "a group of your own", is answered; a named group is still refused rather
357+
* than silently turned into a different one.
358+
*
359+
* ⚠️ That is exactly the case the consumer writes: `setpgid(0, 0)' in the
360+
* child so that a timeout can kill the whole tree. */
361+
if (attr && (attr->__flags & ~(POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK
362+
| POSIX_SPAWN_SETPGROUP)))
363+
return ENOSYS;
364+
if (attr && (attr->__flags & POSIX_SPAWN_SETPGROUP) && attr->__pgrp != 0)
320365
return ENOSYS;
321366
/* ⚠️ musl carries the PATH SEARCH in this field: `posix_spawnp' stores
322367
* `__execvpe' there and its `posix_spawn' calls it in the duplicate instead
@@ -373,6 +418,11 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
373418
struct kal_file opened[OKM_SPAWN_MAX_OPEN];
374419
int opened_n = 0;
375420

421+
/* The directory the program is to run in, if a file action named one. It is
422+
* this call's and is released with the opened files below. */
423+
struct kal_dir where;
424+
int where_held = 0;
425+
376426
/* Resolving a file action's name needs one of these and it is four kilobytes.
377427
* Static, under this file's lock, for the same reason the argument vectors
378428
* above are: a second one on the stack would double the frame of a function
@@ -441,8 +491,33 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
441491
if (op->fd > 2) break;
442492
refused = ENOSYS;
443493
break;
444-
case FDOP_CHDIR:
445-
case FDOP_FCHDIR:
494+
/* ⭐⭐ ANSWERED SINCE 0.12, BECAUSE openkal 0.11 GAVE A SPAWN A SECOND
495+
* DIRECTORY. Both of these say the same thing --- run the program
496+
* HERE --- and until there was a place to put it they were refused
497+
* along with everything else this file could not express. */
498+
case FDOP_CHDIR: {
499+
struct okm_at cat;
500+
const int r = okm_resolve(AT_FDCWD, op->path, &cat, 0);
501+
if (r) { refused = (int)-r; break; }
502+
struct kal_dir d;
503+
const int e = okm_fs_open_dir(cat.base, cat.rel, slen(cat.rel), &d);
504+
if (e != kal_ok) { refused = okm_errno(e); break; }
505+
if (where_held) okm_fs_close_dir(where);
506+
where = d; where_held = 1;
507+
break;
508+
}
509+
case FDOP_FCHDIR: {
510+
struct okm_desc* dd = okm_desc_of(op->fd);
511+
if (!dd || dd->kind != OKM_DIR) { refused = EBADF; break; }
512+
/* A directory of this program's own, opened again so that the
513+
* spawn holds one the caller cannot close underneath it. */
514+
struct kal_dir d;
515+
const int e = okm_fs_open_dir(dd->dir, ".", 1, &d);
516+
if (e != kal_ok) { refused = okm_errno(e); break; }
517+
if (where_held) okm_fs_close_dir(where);
518+
where = d; where_held = 1;
519+
break;
520+
}
446521
default:
447522
/* An action openkal cannot express. Performing the spawn
448523
* without it would start the program in a state the caller did
@@ -468,6 +543,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
468543

469544
if (refused) {
470545
for (int i = 0; i < opened_n; i++) okm_fs_close_file(opened[i]);
546+
if (where_held) okm_fs_close_dir(where);
471547
okm_unlock();
472548
return refused;
473549
}
@@ -476,7 +552,22 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
476552
* started program reads its own name through kal_env_arg(0), so a caller
477553
* that did not supply it could not predict what the program would read. */
478554
struct kal_process child;
479-
int e = start_program(bound, &at, a_ptr, a_len, argc,
555+
/* ⚠️ THE UNIT IS THIS CALL'S, AND IT IS NOT KEPT ANYWHERE AFTERWARDS --- see
556+
* the note at the end of this function. */
557+
struct kal_job unit = { 0 };
558+
struct kal_job* want_unit =
559+
(attr && (attr->__flags & POSIX_SPAWN_SETPGROUP)) ? &unit : 0;
560+
/* ⭐ THE WORKING DIRECTORY IS THIS LIBRARY'S, AND THAT IS THE WHOLE FIX.
561+
*
562+
* `chdir' here moves `okm_cwd_dir' and nothing else, because openkal has no
563+
* operation that moves a running program's. Before 0.11 a spawn carried one
564+
* directory --- the one the program's NAME resolved under --- so a caller that
565+
* chdir'd and then started a program found the program running somewhere
566+
* else entirely. Passing it now closes that, and it closes the `fork' route
567+
* as well: a copy that chdir'd has its own `okm_cwd_dir', and the `execve'
568+
* it then performs reaches this line. */
569+
int e = start_program(bound, want_unit, where_held ? where : okm_cwd_dir,
570+
&at, a_ptr, a_len, argc,
480571
e_ptr, e_len, envc, &streams, &child);
481572

482573
/* ⭐ THE ONE ENVIRONMENT THAT SPELLS A PROGRAM WITH A SUFFIX IS ANSWERED
@@ -505,11 +596,12 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path,
505596
* from a file, and openkal-musl asks the specification to say so for streams
506597
* in general rather than for one of them. */
507598
for (int i = 0; i < opened_n; i++) okm_fs_close_file(opened[i]);
599+
if (where_held) okm_fs_close_dir(where);
508600

509601
okm_unlock();
510602
if (e != kal_ok) return okm_errno(e);
511603

512-
const int pid = __okm_child_record(child);
604+
const int pid = __okm_child_record_job(child, unit);
513605
if (pid < 0) { okm_process_close(child); return EAGAIN; }
514606
*res = (pid_t)pid;
515607
return 0;

0 commit comments

Comments
 (0)