Repository navigation
Three absences a consumer met: a stream at position n, a handle that is zero, and whether a node may be executed #28
Description
Three absences that a C library above this interface cannot compose around. All
three came out of mcpplibs/openkal-linux#13 — six rounds with a consumer porting
a POSIX-style CLI onto the native path — and all three are recorded in
openkal-musl's limits table today as refusals rather than as gaps, because a
facility that reports success and does nothing is the one answer that leaves a
program wrong without telling it. That issue is closed; these are what it produced
and did not answer.
None of them is urgent. Each is filed so that the next implementation meets it in
the specification rather than in a consumer's test suite.
1. A stream at position n in a program being started
What a program observes. A started program receives standard input, output and
error and nothing else. A descriptor 4 that is not close-on-exec is not there,
and fcntl(F_SETFD, 0) upon one therefore changes nothing.
Why it cannot be composed. kal_spawn_streams has exactly three positions, and
there is no general form for placing a stream at an arbitrary position. The port
already refuses posix_spawn_file_actions_adddup2 above position two rather than
accepting it, so the two halves agree with each other; what is lost is implicit
inheritance, which POSIX gives and no composition here can reconstruct.
What would answer it. A form in which the caller names positions and streams
together rather than filling three fixed slots — the same shape kal_spawn already
takes for base and work.
2. Starting a program upon a stream whose handle is zero
What a program observes. A caller that redirects its own output onto its own
standard input and then starts a program gets ENOSYS.
Why it cannot be composed. kal_spawn_streams reserves zero to mean inherit,
and kal_stream reserves nothing — so an implementation whose streams are the
environment's own descriptors hands out zero for standard input. The two meanings
coincide at position zero and cannot be told apart anywhere else. The port refuses
here rather than answering wrongly, because accepting it would hand a program the
stream its caller had just redirected away.
What would answer it. Either a distinguished value for no stream / inherit
that no kal_stream can ever be, or a statement that zero is never a valid stream
handle — one sentence in the interface, and the ambiguity is gone.
3. Whether a node may be executed
What a program observes. access(path, X_OK) answers yes for anything that
exists, and starting a name that exists and cannot be run still ends the caller
with 127.
Why it cannot be composed. kal_node_info carries writable and no other
permission, so it is there is the whole of what a port can answer. The two halves
are one gap: the enquiry cannot tell, and neither can the check a spawn makes before
starting.
What would answer it. This one has a cheaper half. The interface needs no
permission model — that was declined for good reason and the reasoning stands (a FAT
volume, a UEFI system partition and a Windows ACL do not share one). But an
implementation knows when the thing it is asked to start cannot be started:
openkal-linux's own execve is the call that fails. Reporting that distinctly,
rather than as the caller's 127, would answer the second half without the interface
acquiring a mode word. The first half — an enquiry that answers before the attempt —
needs a word here, and may not be worth one.
⭐ Grouped rather than split into three because they share a shape: each is a place
where a declaration would settle what no composition above the line can. Two of
the four defects the same consumer found — a directory's modification time and the
working directory a started program runs in — had exactly this shape, and both went
away when the declaration arrived (kal_fs_set_modified_at in 0.10, kal_spawn's
second directory in 0.11) rather than when someone found a cleverer composition.
Activity
yspbwx2010 commentedon Sep 4, 2026
A measurement for the third item, on the half you called cheaper. The implementation does report a start that could not happen; what the report loses on the way up is the reason, so the caller still cannot act on it.
The shape. A file that exists, is executable (mode 755 on disk, set through a native chmod helper and read back with the host's
stat, since the port declineschmod), and is neither an ELF image nor a#!script: a one-line shell script without the first line. Started withposix_spawnon x86_64, native path, static, openkal-musl 0.13.1 over openkal-linux 0.12.0 (openkal-llvm-runtime 0.9.1). What a program observes:posix_spawnreturns 5 (EIO) and the caller is given no process. On a glibc 2.44 host the same call returns 8 (ENOEXEC) and the caller is given no process, and ENOEXEC is the answerexecvpis documented to act on, and the one any caller ofposix_spawnmust see to do the same: run the file through/bin/shinstead. On the native path that fallback is unreachable, not because the failure is hidden but because it arrives as an I/O error, which no caller would retry as a script. This also differs from the sentence in item 3 (and in the port's README) that such a start "still ends the caller with 127": on this path nothing ends with 127; the caller gets EIO fromposix_spawnand no pid.Where the reason is lost, read from the two sources. The kernel's ENOEXEC does reach the caller: the image writes it into the report pipe (
src/process.cpp, thereport.say(why)that followsexecveat), the caller reads it (report.heard()) and hands it totranslate.translateinsrc/sys.hmaps the errno values the vocabulary has a word for and folds everything else tokal_err_ioin itsdefaultarm; ENOEXEC is not among its cases, and the value is not defined in that file at all. By the time the port'sokm_errno(port/src/okm_fd.c) sees the value it is alreadykal_err_io, and that function's owndefault: return EIOnever gets a turn. Both tables lack the word; the first is where the fold happens.So for the second half of item 3, "reporting that distinctly, rather than as the caller's 127": the report path already exists, and what it lacks is one value in
kal_error(thirteen today,include/openkal/types.h) for "the name is there and may be run, but is not in a form this environment can start".kal_err_permissionalready carries the 644 case from the earlier rounds, since EACCES has a word in both tables and arrives intact; this is the neighbouring case, and it is the one a POSIX C library composesexecvpon. Whether the enquiry half,access(X_OK)before the attempt, is worth a word I have no measurement to offer.For the note, the chain, with the first two steps read from the sources and the last one measured: the kernel's ENOEXEC (8), which
translatefolds tokal_err_io(3), whichokm_errnohands back as EIO (5).Sunrisepeak commentedon Sep 17, 2026
Answered in openkal 0.13.0 (#33), item by item.
kal_preopenis. openkal-musl continues to refuseadddup2above position two, so nothing is silently wrong.KAL_INFO_EXECUTABLE,KAL_FS_PROP_EXECUTABLEandkal_fs_set_executable_atrecord whether a node may be started. This is a property of the node, not a permission (clause 11 item 6).kal_err_not_programis added (clause 5.2), andkal_process_spawnstates that a start that did not happen is reported by its reason.ENOEXEC, openkal-macos 0.10.0 reports failed starts (it used to returnkal_okfor every name that could not be started), and openkal-windows 0.8.0 mapsERROR_BAD_EXE_FORMAT.ENOEXECfromposix_spawn, answersstatandaccess(X_OK)from the record, and performschmodof the execute bits.The measurement in the comment above was the one the error value was designed against. The same reasoning appears in the openkal-musl 09-04 analysis committed with 0.14.0.