Skip to content

Three absences a consumer met: a stream at position n, a handle that is zero, and whether a node may be executed #28

Closed

Description

@Sunrisepeak

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

  1. yspbwx2010 commented on Sep 4, 2026

    @yspbwx2010

    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 declines chmod), and is neither an ELF image nor a #! script: a one-line shell script without the first line. Started with posix_spawn on 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_spawn returns 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 answer execvp is documented to act on, and the one any caller of posix_spawn must see to do the same: run the file through /bin/sh instead. 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 from posix_spawn and 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, the report.say(why) that follows execveat), the caller reads it (report.heard()) and hands it to translate. translate in src/sys.h maps the errno values the vocabulary has a word for and folds everything else to kal_err_io in its default arm; ENOEXEC is not among its cases, and the value is not defined in that file at all. By the time the port's okm_errno (port/src/okm_fd.c) sees the value it is already kal_err_io, and that function's own default: return EIO never 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_permission already 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 composes execvp on. 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 translate folds to kal_err_io (3), which okm_errno hands back as EIO (5).

  2. Sunrisepeak commented on Sep 17, 2026

    @Sunrisepeak
    MemberAuthor

    Answered in openkal 0.13.0 (#33), item by item.

    1. A stream at position n. Considered and not defined; SPEC clause 11 entry 18. A numbered position is one environment's descriptor table, and a started program would still need to be told where to look. If the need is measured, the shape consistent with the specification is a named grant, as kal_preopen is. openkal-musl continues to refuse adddup2 above position two, so nothing is silently wrong.
    2. A stream handle of zero. Recorded, not changed; clause 11 entry 19. An implementation may remove the ambiguity by never answering a stream enquiry with zero, and the specification does not require it. openkal-musl reports the request as unsupported.
    3. Whether a node may be executed.
      • Setting it. KAL_INFO_EXECUTABLE, KAL_FS_PROP_EXECUTABLE and kal_fs_set_executable_at record whether a node may be started. This is a property of the node, not a permission (clause 11 item 6).
      • Reporting why a start failed. kal_err_not_program is added (clause 5.2), and kal_process_spawn states that a start that did not happen is reported by its reason.
      • Implementations. openkal-linux 0.13.0 maps ENOEXEC, openkal-macos 0.10.0 reports failed starts (it used to return kal_ok for every name that could not be started), and openkal-windows 0.8.0 maps ERROR_BAD_EXE_FORMAT.
      • Above them. openkal-musl 0.14.0 returns ENOEXEC from posix_spawn, answers stat and access(X_OK) from the record, and performs chmod of 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions