Skip to content

0.10.0 — the exec search, and the family of answers that were not true - #22

Merged
Sunrisepeak merged 4 commits into
mainfrom
fix/issue13-exec-search-locks-and-identity
Aug 30, 2026
Merged

0.10.0 — the exec search, and the family of answers that were not true#22
Sunrisepeak merged 4 commits into
mainfrom
fix/issue13-exec-search-locks-and-identity

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

openkal-linux#13, fifth round.

A consumer measured that execvp could not get past the first PATH entry that
missed, and that one libc path accounted for nine of their nineteen failures
plus four sections silently skipped (bwrap, installed at /usr/bin/bwrap,
reported as not installed). That is fixed — and reading it out found six more
of the same family
, one of which is worse than the one reported.

⭐⭐ What they have in common, and why no diagnostic here could see them

Every probe in this repository asked whether an operation worked. None asked
whether an answer was true. OPENKAL_MUSL_TRACE=enosys reports what reaches
the default arm, and not one of these reaches it — they are operations that are
present and answer wrongly.

examples/surface is the probe that asks the other question, by comparing every
answer against the host's. It is how the six were found.

Reported by the consumer

execve did not return when the program could not be started the replacement happens inside a duplicate, whose failure openkal-linux reports to nobody; the duplicate ended with 127 and execve ended the caller with it. __posix_spawn now asks whether the name can be started first
posix_spawnp never searched a PATH at all musl hands __execvpe to posix_spawn to run in the duplicate; this port has no duplicate, so the field was read by nobody and posix_spawnp("sh", …) started ./sh and reported success. musl's source is now replaced — the tenth

⚠️⚠️ Found by reading it out — a lock was granted and never taken

fcntl(F_SETLK), F_SETLKW and F_GETLK answered 0 and did nothing. Measured
against the host: two programs took one exclusive lock and both were told they
had it.
F_GETLK pointed the other way — it left the caller's word untouched,
which reads as "somebody holds this", for ever, so a loop waiting for a lock
never left it. All three report ENOSYS.

⭐ The refusal is temporary in a way the permission one is not: every
environment beneath openkal can lock a byte range (fcntl, LockFileEx), and
what is missing is a word in the specification. Composing one here from
KAL_OPEN_EXCLUSIVE is not an option — nothing would release it when its holder
died.

The rest

  • ⚠️ getppid returned -38 as an identifier — the defect getpgrp had one
    release earlier, three lines away in the same dispatch, not looked for. The
    criterion added here asks the whole family.
  • A copy made by fork reported its parent's identifier. Settled before the
    copy is taken and carried into it — and the comparison kill makes moved with
    it, or abort in a copy would have reported ESRCH.
  • setpgid(0,0) and setsid refused a question neither asks → 0 and EPERM.
    Every daemonising library handles EPERM; none handles ENOSYS.
  • sigaltstack reported an installation it had not performed → ENOSYS.
  • sysconf(_SC_OPEN_MAX) answered 0 for a bound this library sets itself.
  • utimensat could not set a directory's time — reported as a failure to
    read one, because libc++ gives both overloads of last_write_time one name.
    Reading was never broken.

What is NOT fixed, and is now recorded

kill does not reach a program started by fork + execve — it reaches the
copy that is waiting for it. Measured with the host as control: identical status
words, opposite outcomes. openkal has no way to say "this program's lifetime is
bound to mine", and kal_process_terminate is right to terminate only what it
was given. Asked of the specification; README says to use posix_spawn,
system or popen where a caller needs to stop what it started.

⚠️ README.md and musl/PATCHES.md claimed a caller cannot distinguish
execve-as-spawn+wait from a real replacement. That claim was false and is what
kept anyone from looking. Both now list the three known differences.

Criteria

  • examples/surface is new: eleven of its observations fail on 0.9.0 and none
    does here.
  • examples/subprocess gains the exec search and the identity. On 0.9.0 it does
    not merely fail — it stops, at the execve observation, which is the
    defect itself.
  • ⭐ Two are guards rather than criteria (they hold before this change too):
    abort in a copy is still SIGABRT, and a failed fork gives its table entry
    back. Listing them beside the criteria would have made five of seven
    observations look like results.
  • ⚠️ examples/posix asserted that setting a directory's time is refused, and
    its own comment said that observation would be the one to say the row was out
    of date. It was.

openkal-linux#13, fifth round. A consumer measured that `execvp` could not get
past the first PATH entry that missed, and that one libc path accounted for nine
of their nineteen failures. It is fixed --- and reading it out found six more of
the same family, one of which was worse than the one reported.

⭐⭐ WHAT THEY HAVE IN COMMON, AND WHY NO DIAGNOSTIC HERE COULD SEE THEM.

Every probe in this repository asked whether an operation WORKED. None asked
whether an answer was TRUE. `OPENKAL_MUSL_TRACE=enosys` reports what reaches the
default arm, and not one of these reaches it: they are operations that are
present and answer wrongly. `examples/surface` is the probe that asks the other
question, by comparing every answer against the host's.

--- what a consumer reported -----------------------------------------------

`execve` did not return when the program could not be started. The replacement
happens inside a duplicate, whose failure openkal-linux reports to nobody, so
the duplicate ended with 127 and `execve` ended the CALLER with it. musl's
`execvp` issues one `execve` per PATH entry and needs each to return.

  ⇒ `__posix_spawn` asks whether the name can be started before starting it.
    ENOENT and EACCES reach the caller; a name that exists and cannot be
    executed still does not, because openkal reports no execute permission ---
    recorded, and asked of openkal-linux, which knows and does not report it.

`posix_spawnp` never searched a PATH at all. musl hands `__execvpe` to
`posix_spawn` to run in the duplicate; this port has no duplicate, so the field
was read by nobody and `posix_spawnp("sh", ...)` started `./sh` and REPORTED
SUCCESS. musl's source is now replaced, the tenth, and the search is performed
here by musl's own rules.

--- what reading it out found ----------------------------------------------

⚠️⚠️ A LOCK WAS GRANTED AND NEVER TAKEN. `fcntl(F_SETLK)`, `F_SETLKW` and
`F_GETLK` answered 0 and did nothing. Measured against the host: two programs
took one exclusive lock and BOTH were told they had it. `F_GETLK` pointed the
other way --- it left the caller's word untouched, which reads as "somebody
holds this", for ever, so a loop waiting for a lock never left it. All three
report ENOSYS.

  ⭐ The refusal is temporary in a way the permission one is not: every
    environment beneath openkal can lock a byte range, and what is missing is a
    word in the specification. Composing one here from KAL_OPEN_EXCLUSIVE is not
    an option --- nothing would release it when its holder died.

⚠️ `getppid` RETURNED -38 AS AN IDENTIFIER, which is the defect `getpgrp` had one
release earlier, three lines away in the same dispatch, and it was not looked
for. The criterion added here asks the whole family.

A copy made by `fork` reported its parent's identifier: `getpid` answered the
constant 1 in every context. It is settled before the copy is taken and carried
into it --- and the comparison `kill` makes to decide "this program itself"
moved with it, or `abort` in a copy would have reported ESRCH.

`setpgid(0, 0)` and `setsid` refused a question neither asks. Being in a group of
one already holds here, and `setsid` has a failure POSIX writes down for that
state. 0 and EPERM. Every daemonising library handles EPERM; none handles ENOSYS.

`sigaltstack` reported an installation it had not performed, and the enquiry
answered 0 with a zeroed record. ENOSYS.

`sysconf(_SC_OPEN_MAX)` answered 0 for a bound this library sets itself.

`utimensat` could not set a DIRECTORY's time, which a consumer reported as a
failure to READ one --- libc++ gives both overloads of `last_write_time` one
name. Reading was never broken. The port asked for READ|WRITE unconditionally;
it now asks what the name refers to. ⚠️ Outside what fs.h states, recorded, and
asked of the specification.

--- what is NOT fixed, and is now recorded ---------------------------------

`kill` does not reach a program started by `fork` + `execve`: it reaches the
copy that is waiting for it. Measured with the host as control --- identical
status words, opposite outcomes. openkal has no way to say "this program's
lifetime is bound to mine", and `kal_process_terminate` is right to terminate
only what it was given. Asked of the specification. README says to use
`posix_spawn`, `system` or `popen` where a caller needs to stop what it started.

Also recorded: `access(X_OK)` answers yes for anything that exists; descriptors
above 2 do not cross into a started program; `sched_getaffinity` is absent so
`hardware_concurrency()` answers 1 SILENTLY; `statvfs`, `link`, `mkfifo` and
`socketpair` have no operation beneath.

--- criteria ---------------------------------------------------------------

`examples/surface` is new: eleven of its observations fail on 0.9.0 and none
does here. `examples/subprocess` gains the exec search and the identity, and
on 0.9.0 it does not merely fail --- it STOPS, at the `execve` observation,
which is the defect itself.

⭐ Two of them are guards rather than criteria: they hold before this change as
well. `abort` in a copy is still SIGABRT, and a failed `fork` gives its table
entry back. Listing them beside the criteria would have made five of seven
observations look like results.

⚠️ `examples/posix` asserted that setting a directory's time IS refused, and its
own comment said that observation would be the one to say the row was out of
date. It was. The half that depends on the backend moved to `examples/surface`,
under `--dir-time`, and the reading half stayed.
⚠️⚠️ `posix_spawnp' IS THE TENTH REPLACED SOURCE AND TWO OTHER LISTS SAY WHAT
THE FIRST ONE SAYS.

`tools/probe-cross-macos.sh' and `tools/cross-build-macos.sh` each keep their own
copy of musl's source selection, because the linker they drive does not read
mcpp.toml. Both matched `posix_spawn' anchored on the whole basename, so neither
covered `posix_spawnp.c', and the cross-link reported

    ld64.lld: error: duplicate symbol: _posix_spawnp

⭐ Found by the job whose whole purpose is to notice that a second statement has
fallen behind the first --- on the first release that made it fall behind. The
warning is now written beside the entry rather than left to be rediscovered.

--- and two the reading found ----------------------------------------------

⚠️ AN ENQUIRY THAT CANNOT BE MADE IS NOT AN ANSWER OF `NO'. `startable' and the
directory branch of `utimensat' both ask `kal_fs_info' first, and a build
configured without `openkal.fs' --- OKM_HAS_FS=0, which a machine with no storage
uses --- answers `not supported'. Turning that into a refusal would have stopped
a spawn this port would otherwise have attempted. Both now fall through to the
operation that follows, which answers as it did before the enquiry existed.

`__okm_self_pid' was written and never called. Removed.

And `slot`/`reserved_pid` in `__okm_fork' are `volatile`. Both are written BEFORE
the `setjmp' and read only on the path that does not resume through it, so this
is not a correctness fix --- it is this file's own rule, which is that a local
live across that call says so rather than leaving a reader to reconstruct which
path reads which.
…use it

⚠️ One environment separates its own PATH with a semicolon and begins each entry
with a volume letter and a colon, so reading that PATH on a colon produces
entries that are not names.

It is still a colon, because `execvp' --- which this port does NOT replace, and
which reaches `execve' --- splits on one in musl's own source. Splitting
differently in `__posix_spawnp' would make the two ways of searching for one
program disagree with each other, which is worse for a caller to meet than one
way both are wrong in. Recorded rather than quietly differing; nothing on that
target searches a PATH today, because its row declares no shell.
⚠️ `getenv' answers a pointer INTO the environment and the `setenv' that follows
may move it, so the PATH the probe replaces has to be kept somewhere. It was
kept in 1024 bytes and restored from them --- and a continuous-integration
machine's PATH is longer than that, so what would have been restored is a
TRUNCATED PATH: the probe quietly corrupting the environment for whatever
observation is added after it.

Kept by the value's own length instead. Found by re-reading the diff rather than
by anything failing, which is what it would have done --- later, and to someone
else's observation.
@Sunrisepeak
Sunrisepeak merged commit 2601979 into main Aug 30, 2026
6 checks passed
@Sunrisepeak
Sunrisepeak deleted the fix/issue13-exec-search-locks-and-identity branch August 30, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant