Skip to content

0.11.0 — what openkal 0.10 made possible, and two of them were recorded as impossible one release ago - #24

Merged
Sunrisepeak merged 14 commits into
mainfrom
declarations-0.10
Aug 30, 2026
Merged

0.11.0 — what openkal 0.10 made possible, and two of them were recorded as impossible one release ago#24
Sunrisepeak merged 14 commits into
mainfrom
declarations-0.10

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Part of the openkal 0.10 move: mcpplibs/openkal#25.

⭐⭐ Two entries in this package's own divergence table are deleted rather than amended, and they are the two it recorded as not fixable here.

A lock now excludes

0.10.0 refused fcntl(F_SETLK) with ENOSYS and said the refusal was temporary in a way the permission row is not. openkal 0.10 is the word that was missing.

parent  F_SETLK(F_WRLCK) -> 0    ACQUIRED
child   F_SETLK(F_WRLCK) -> -1   EAGAIN
=> only one holds it

⭐ And the observation that tells the two lock forms apart needs no second program: a second OPEN FILE of one name, in this program, is refused. The process-held form grants it, and a library that opened one file twice would destroy its own lock.

⚠️ F_GETLK is still refused — it asks whether a lock would block without taking one, and openkal has no operation that answers a question without performing it.

And kill now reaches the program

fork + execve   status=0x000f   program was killed
posix_spawn     status=0x000f   program was killed

execve — and only execve — asks for kal_process_spawn_bound. posix_spawn means the opposite: a POSIX child outlives its parent.

Three more that were absent

sched_getaffinity answered ENOSYS, so hardware_concurrency() answered 1 silently; now 32 on a machine with 32. statfs had nothing beneath it. utimensat on a directory took a route outside anything the interface stated, and now takes the stated one.

…rded as

impossible one release ago

⭐⭐ TWO ENTRIES IN THIS PACKAGE'S OWN DIVERGENCE TABLE ARE DELETED RATHER THAN
AMENDED, AND THEY ARE THE TWO IT SAID COULD NOT BE FIXED HERE.

--- a lock now excludes -------------------------------------------------------

0.10.0 refused `fcntl(F_SETLK)' with ENOSYS, and said the refusal was TEMPORARY
in a way `chmod' is not: every environment beneath openkal can lock a byte range
and what was missing was a WORD. openkal 0.10 is that word.

Measured, with the host as control, and it is the reading that used to be the
other way round:

    parent  F_SETLK(F_WRLCK) -> 0    ACQUIRED
    child   F_SETLK(F_WRLCK) -> -1   EAGAIN
    => only one holds it

⭐ AND THE OBSERVATION THAT TELLS THE TWO FORMS APART NEEDS NO SECOND PROGRAM:
a SECOND OPEN FILE of one name, in this program, is refused. The process-held
form would grant it --- the holder being the process, which already holds it ---
and a library that opened one file twice would destroy its own lock. openkal
states the holder as the open FILE, and `examples/surface' now asserts exactly
that.

⚠️ `F_GETLK' IS STILL REFUSED, AND THAT IS NOT AN OVERSIGHT. It asks whether a
lock WOULD block without taking one, and openkal has no operation that answers a
question without performing it --- the absence clause 6.3 records for readiness.
Taking the lock and releasing it would answer, and would also take a lock the
caller did not ask for, hand a spurious `no' to a caller that already holds one,
and be stale on return.

--- and `kill' now reaches the program ----------------------------------------

`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. A signal aimed at the
identifier the caller holds reached the WAITER: identical status words, opposite
outcomes, the caller told the program died while the program ran to completion.
0.10.0 recorded this as NOT fixable here and told callers to use `posix_spawn'.

openkal 0.10's `kal_process_spawn_bound' says the thing that could not be said,
and `execve' --- and only `execve' --- asks for it. `posix_spawn' means the
opposite: a POSIX child outlives its parent.

    fork + execve   status=0x000f   program was killed
    posix_spawn     status=0x000f   program was killed

⚠️ A backend may decline the binding, and then this falls back to the unbound
spawn rather than refusing to start the program. An `execve' whose program is
unbound is where this port has always been; an `execve' that starts nothing is
worse.

--- three more that were absent -----------------------------------------------

`sched_getaffinity' answered ENOSYS, so `sysconf(_SC_NPROCESSORS_ONLN)' fell back
to 1 and `hardware_concurrency()' answered 1 SILENTLY. Now 32 on a machine with
32. ⚠️ Zero from the enquiry is reported as a refusal rather than as a bitmap of
one processor: "cannot say" is not "one", and musl would read the latter as a
fact this port had invented.

`statfs' had nothing beneath it, so `std::filesystem::space' reported ENOSYS.

`utimensat' upon a DIRECTORY reached it by opening the directory for READING and
setting the time on that --- which worked and was outside anything the interface
stated. It now takes `kal_fs_set_modified_at', which is the stated route. The old
way is kept for a backend that has not followed, and is tried only where the new
operation is absent.

--- and one thing that did NOT change -----------------------------------------

⚠️ `l_whence == SEEK_END' is refused. openkal takes a position and a length; the
length a range would be measured back from is not something the interface reports
at the moment the lock is taken, and computing it from a size that may already
have changed is the kind of answer this port exists to refuse.
⚠️⚠️ TWO ROWS ARE DELETED RATHER THAN AMENDED, AND THEY ARE THE TWO THIS PACKAGE
RECORDED AS NOT FIXABLE HERE.

The lock row said its refusal was TEMPORARY in a way the permission row is not,
and that when the specification gained the word the row would change. It has.
The `execve' paragraph said a `kill' reaching the waiting copy was 'not
answered' and told callers to use `posix_spawn' instead; `kal_process_spawn_bound'
answers it and the advice is withdrawn.

⭐ A record that says what WOULD change it, and then changes when that happens,
is the difference between a divergence table and a list of complaints. Both rows
named the thing they were waiting for.

Volume capacity leaves the 'no operation beneath' row; hard links, named pipes
and a bidirectional pair stay in it, because openkal still has no operation for
those and this package is not going to invent one.
⭐ A CONSEQUENCE OF THE SPECIFICATION ADDITION THAT THE ADDITION ITSELF DID NOT
PREDICT, AND CI FOUND IT.

The matrix row for that system carried `--no-dir-time' because `kal_fs_open'
there names `FILE_NON_DIRECTORY_FILE' --- correctly, since it opens a FILE --- so
the route 0.10.0 used (open the directory for reading, stamp that) could not
work.

openkal 0.10's `kal_fs_set_modified_at' takes a NAME, and the Windows
implementation opens for the ATTRIBUTE alone. That reaches a directory. So the
observation that asserted a refusal there is now asserting something false, and
the row flips to `--dir-time'.

⚠️ The divergence this README row recorded was caused by a MISSING DECLARATION,
not by a property of that system --- which is why it went away when the
declaration arrived rather than being worked around.
⚠️⚠️ THE ELEVENTH REPLACED SOURCE, AND THE THIRD OF A KIND THIS PORT ALREADY
NAMES TWICE.

    unsigned long arg;  arg = va_arg(ap, unsigned long);
    case F_SETLK: return syscall(SYS_fcntl, fd, cmd, (void *)arg);

An `unsigned long' holds a pointer on every system musl was written for and
thirty-two bits on one this port builds for, so a `struct flock *' arrived with
its top half discarded BEFORE THIS PORT SAW IT.

⭐ IT WAS UNREACHABLE UNTIL THIS RELEASE, which is why it survived three of them.
Every command answered before took an integer or took a pointer nothing followed:
`F_SETLK' returned 0 and did nothing, then reported ENOSYS. A truncated pointer
that nothing dereferences is a truncated pointer nothing reports. openkal 0.10
gave this port a real lock, and it faulted on the first attempt.

⚠️ Measured under an emulator of that system, and the register file names the
TYPE rather than the symptom:

    page fault on read access to 0x00000000fe2ffec2
    rax:00000000fe2ffec0   rsp:00007ffffe2fc7a0
    movzxw 0x02(%rax), %eax

`rax' is the caller's pointer with its top half gone; the offset it faults at is
`l_whence', the first field this port reads.

⚠️ AND THE VARARG TYPE IS THE CALLING CONVENTION, NOT A DETAIL. `va_arg(ap,
unsigned long)' and `va_arg(ap, uintptr_t)' read different numbers of bytes where
the two differ, so a cast afterwards is too late.

⭐ Both macOS cross lists carry the new exclusion in the same commit. That is the
lesson of the tenth entry, which they did not carry and which the cross-link job
reported as a duplicate symbol.
The project behind openkal-linux#13 is local now, so this is measured against it
rather than against probes.

⭐ CROSS-COMPILATION IS THROUGH THE NATIVE PATH FOR BOTH ARCHITECTURES, and their
`scripts/build-static.sh' --- which opens by saying it bypasses mcpp's musl
target --- can retire. x86_64 and aarch64 both link statically, zero INTERP, zero
undefined symbols. aarch64 was not a compilation problem: three lines were
missing from the manifest.

⚠️⚠️ `chdir' DOES NOT REACH A STARTED PROGRAM, measured with a host as control.
This library's `chdir' rebinds its own table because openkal has no operation
that changes a running program's working directory, and the backend's
`execveat' uses its directory only to RESOLVE THE NAME. So a started program
runs where the whole program was started, whatever the caller did --- and
openkal-linux's own comment claims the opposite property in as many words.

⚠️ THERE ARE NO PROCESS GROUPS, so a timeout cannot kill a tree. Their logic is
the standard one and the direct child does die; what survives is what bash
backgrounded. This is the alternative I rejected when designing
`kal_process_spawn_bound' --- the rejection still holds, but it shows that
killing a TREE is a thing consumers need and that the bound spawn answers only
the first layer.

⭐ None of the three is in issue 13's list, and none is in any criterion I added
this round. A criterion is written against a defect already known; a consumer's
tests are written against what they are trying to do.
…at a time

⚠️⚠️ MEASURED AGAINST THE HOST, ON `echo one; sleep 0.4; echo two':

    here    "o" "n" "e" "." "t" "w" "o" "."      eight chunks
    host    "one." "two."                        two

openkal has no operation reporting whether a transfer would proceed, so a
readiness enquiry TAKES A BYTE to make its answer true, and `read' delivers it.
Returning only that byte is a legal short read --- and a caller that polls goes
straight back to `poll', which takes another byte. So the whole of a stream
arrives ONE BYTE PER ITERATION, for ever.

⭐ EVERY BYTE IS DELIVERED AND IN ORDER, WHICH IS WHY IT SURVIVED. A caller that
concatenates sees exactly the right bytes; the defect is invisible to anyone who
does not look at the BOUNDARIES. A caller that scans a chunk for a word finds
none, because `two' arrives as `t' and `wo'.

⭐⭐ AND IT IS WHAT openkal-linux#13's FIRST REPORT MEANT BY "only output one
byte". That line has been in the issue since the beginning and nothing explained
it. This is it.

⇒ The remedy is the operation the enquiry is built on: a bound of `now' asks for
whatever has already arrived and does not wait, so the byte and the rest of what
is there come back together. Where the environment declines `openkal.timeout'
there is no such operation and the single byte is what can be honestly returned.

Measured through the reporter's own suite rather than a probe: their
`test_process_stream' had two observations red, and both are green ---
`增量: 两段各自到达' and `增量: 第二段晚于第一段 ≥200ms(真流式)', the second of
which their run had never even reached.
…0.11

The one-byte read is fixed and §7.4 now says what it was: `poll' held a single
byte, so a polled stream arrived a byte at a time --- which is what
openkal-linux#13's "only one byte" meant. Nothing was truncated; I had read the
report wrong.

§7.5 is new: aarch64 had never been run at all, and the reason was three lines
of manifest rather than anything about the architecture. A `cfg` predicate
covers both, the artefacts are static, and the host's binfmt_misc runs them --- no
virtual machine. Both architectures fail identically, which is itself a reading.

The 0.11 note is a proposal and nothing more. Two gaps arrived together --- saying
where a program runs, and ending what it started --- and `process.h' already
warned where a variant-per-combination ends. It states its own cost: one general
form makes three existing declarations redundant, and clause 8 will not remove
them.
…ueue

answered ENOSYS and musl does not check

`FUTEX_REQUEUE' asks for waiters upon one address to be MOVED to another, so a
context released by a condition variable goes straight to waiting upon the
mutex instead of waking only to block again. openkal has kal_task_wait and
kal_task_wake and nothing that moves a waiter between addresses, so this reached
the default arm and answered ENOSYS.

musl's `unlock_requeue' releases the barrier and then makes that request. When
it fails there is no remaining path that wakes anybody, so the next waiter
sleeps until the program is killed.

⭐ IT TAKES TWO WAITERS, WHICH IS WHY IT SURVIVED. The call is reached only when
a second context is queued behind the one being released; one waiter upon a
condition variable never reaches it, and one waiter is what almost every program
has. A consumer's test that has two contexts wait upon one variable passes on a
host in 0.06s and did not finish here in 300 --- that pair of numbers is the whole
diagnosis, and it is why the host control matters.

Waking is a correct substitute for moving: a waiter is always inside
`while (a_cas(l, 0, 2))', so a context woken upon the source address re-reads
the word, takes the lock the caller has just released, and proceeds. What is
lost is one journey through the scheduler, not an outcome.

Measured after: the same test, 300s timeout to 0.10s.
…ays was

This file starts four contexts and contends a mutex 80000 times, and it passed
throughout a defect that stopped the port dead. Contending a mutex is not
waiting upon a condition variable, and one waiter is not two: musl asks for a
requeue only when a second context is queued behind the one being released, so
a port that cannot requeue costs nothing until a program has two waiters.

⇒ So the observation is not "a condition variable works". It is that the second
waiter is woken. It also checks that both were waiting BEFORE the broadcast,
because a broadcast that arrives first wakes nobody and the observation would
then hold for a reason unrelated to what it checks.

Verified in both directions, which is what makes it a guard rather than a line
that happens to pass: with the requeue case it reports three more `ok's, and
with that case removed the probe stops at exactly this block --- "it did not
return; where it was", immediately after counter=80000.

⚠️ Written at file scope. The obvious spelling puts the waiter beside the two
statics inside the block, and a nested function is a GCC extension that the
llvm half of this repository's CI rejects.
…re written

Four defects came out of the consumer's own tests. Two are gaps in the
specification (7.2, 7.3) and two are this port's use of primitives it already
had (7.4, 7.6) --- and the second pair is the interesting half, because nothing
in the specification had to change for either.

7.6 is the worst of the four and the one the criteria had the least chance of
finding. This repository's own probe starts four contexts and contends a mutex
eighty thousand times, and passed throughout: contending a mutex is not waiting
upon a condition variable, and one waiter is not two. The requeue is requested
only when a second context is queued behind the one being released.

⇒ Records the three pieces of evidence that agree (the ENOSYS arm, musl's
unchecked `unlock_requeue', and a futex value of 2 that names the exact loop),
and the pair of numbers that made it a diagnosis rather than a guess: 0.06s on
a host, unfinished in 300 here.
…changes

Both architectures now agree test by test: 96 pass and 12 fail on each, and the
twelve are eight the project owns and four that are the two remaining
specification gaps. Every attribution has a discriminator behind it rather than
a name that sounds like one --- the same suite run against the host target, which
is what separated test_chat_approval (green there, 300s here, so ours) from
test_chat_live (red there too, so not).
The one-byte fix landed with no observation of its own --- I wrote it up in the
plan as though `examples/subprocess' guarded it, and it did not. Corrected by
adding the guard rather than by softening the claim.

⭐ It deliberately does not check the bytes. Every byte was always delivered and
in order, which is exactly why the defect survived: a caller that concatenates
sees the right output, and only a caller that looks at the BOUNDARIES sees
anything wrong. So the observation is that the FIRST arrival is the segment the
writer wrote, and that the second arrives separately rather than folded into it
--- a reader that got all eight bytes at once would satisfy the first check for
the wrong reason.

Verified in both directions: with the fix both observations hold, and against
the commit before it the probe prints "first arrival was 1 byte(s)" and fails
both. It also prints that number, so a future failure says what it saw.

⇒ §5 gains criteria 8 and 9, and says why neither could have come from the
method that produced 1 to 7: those were written from what the specification
added and from what I might have broken, and not one of them from what a
consumer actually does.
The table says what this port cannot do, and the two things the consumer in
openkal-linux#13 actually ran into were missing from it: the working directory a
started program runs in, and terminating what a started program itself started.
Both were known, analysed and written up in .agents/docs --- and absent from the
file a consumer reads.

⚠️ The first deserves the emphasis it now has, because it is SILENT. `chdir'
reports success, this program's own paths follow it, and only the started
program disagrees; there is no error anywhere to notice.

Both rows say why the port cannot compose the answer, so that "openkal has no
operation for it" is not read as "the port did not bother": a working directory
cannot be set in a copy because openkal deliberately has no operation that moves
a running program's, and a lock built beside the file would be released by
nobody. And both name where the addition is proposed rather than leaving the
reader to wonder whether anyone noticed.
@Sunrisepeak
Sunrisepeak merged commit f5a499d into main Aug 30, 2026
6 checks passed
@Sunrisepeak
Sunrisepeak deleted the declarations-0.10 branch August 30, 2026 11:29
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