Skip to content

0.8.0 --- truncate(2), and the backends that fixed the EAGAIN this port reported - #20

Merged
Sunrisepeak merged 4 commits into
mainfrom
fix/truncate-and-the-timeout-repin
Aug 29, 2026
Merged

0.8.0 --- truncate(2), and the backends that fixed the EAGAIN this port reported#20
Sunrisepeak merged 4 commits into
mainfrom
fix/truncate-and-the-timeout-repin

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Round four of mcpplibs/openkal-linux#13. Three of the reported items are answered, one diagnosis is corrected, and two remain open with the reasoning recorded.

truncate (76) had no case

Only ftruncate was answered. The two are not interchangeable to a caller holding a name and no open file, which is every caller of std::filesystem::resize_file — libc++ expresses it as detail::truncate(p.c_str(), size) (operations.cpp:966).

Composed exactly as SYS_utimensat beside it is: resolve, open for reading and writing, act, release.

Measured in examples/posix, with the case and with it removed again:

ok: the length is set by name
ok: the length set by name is the length reported
FAIL: the length is set by name (errno=38)
FAIL: the length set by name is the length reported (errno=38)

The descriptor is closed before those run, so neither can reach the operation that already worked.

The EAGAIN is real, and it is not here

std::filesystem::copy_file answering Resource temporarily unavailable for two ordinary files was openkal-linux and openkal-macos, fixed in 0.7.1 and 0.6.1, which this repins to. kal_timeout_read decoded a borrowed kal_stream handle with the decoder for an owned one, and waited upon the descriptor below the one it then transferred upon.

This port is where it was observed: libc++ opens a copy's source with O_NONBLOCK, do_read routes that through okm_timed_read, SYS_copy_file_range is a do_read/do_write loop, and EAGAIN is not in libc++'s fallback list. Nothing here needed to change — the route through O_NONBLOCK was correct and what it reached was not.

The previous two rounds attributed this to exhaustion of the started-program table. That is now disposed of: it reproduces on a single copy of one ordinary file.

last_write_time on a directory: the diagnosis is inverted

The report says reading a directory's modification time fails. It does not.

Reading is statkal_fs_infonewfstatat, which resolves a directory. Setting is utimensat, and this port opens the name as a file because kal_fs_set_modified takes a kal_file and openkal has no form of it taking a directory or a name. A directory refuses that open, as EISDIR.

libc++ names both overloads "last_write_time" in the message it throws (operations.cpp:679 and :691), so the text does not say which failed. A lock protocol built on a directory reads the timestamp to decide staleness and writes it to refresh the lock, and only the second fails.

Measured, the two separated:

ok: a directory reports its modification time
ok: setting a directory's modification time is refused, as EISDIR

README.md gains the row, and the refusal is asserted rather than merely not exercised — so the row is contradicted the day openkal gains the operation.

The document

.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md. Two of its findings change what the next round should do:

The crash the report calls unfixed was fixed in 0.7.0, and the build measured did not contain it. The reported trace names operations 88 and 324; neither can be printed by 0.7.0, because symlink/symlinkat have cases, membarrier has a case whose stated purpose is that the trace not report it, and trace_absent is called from the default arm alone. Both numbers are defined for that architecture. The measured library was 0.6.0 — and since openkal-llvm-runtime 0.4.0 requires 0.7.0, the runtime was 0.3.1.

A program built on this port cannot state which version it holds. The only version-shaped thing a program can read is SYS_uname's release field, which is the literal "0.5.0" and has never moved. §7 proposes fixing that; it is deliberately not in this change, being a change to what a program observes.

setpgid/setsid remain absent, and §6 argues they cannot be composed above the line and belong in a specification change: openkal's process interface has no group and no session, and the requirement behind them is ending a started program together with everything it started.

Only `ftruncate' was answered. The two are not interchangeable to a caller
holding a name and no open file, which is every caller of
`std::filesystem::resize_file': libc++ expresses it as
`detail::truncate(p.c_str(), size)'. Reported in openkal-linux#13, where
operation 76 appears in the trace of an ordinary run.

Composed exactly as `SYS_utimensat' beside it is: resolve the name, open the
file for reading and writing, act, release. Opened for both because openkal
decides at the point of opening what may afterwards be done with a file, and the
divergence that produces from POSIX -- which asks for write permission and not
for read -- is the one already recorded there.

--- and two observations about a directory's modification time ---------------

`examples/posix' gains four observations. Two are the operation above, taken
with the descriptor already closed so that nothing can reach the one that
already worked:

    ok: the length is set by name
    ok: the length set by name is the length reported

and with the case removed again they are

    FAIL: the length is set by name (errno=38)
    FAIL: the length set by name is the length reported (errno=38)

The other two record an asymmetry that a consumer read the wrong way round, and
they are here because the C++ library above gives the two overloads one name.
`std::filesystem::last_write_time' throws `filesystem error: in last_write_time'
whether it was reading or writing, so a caller cannot tell from the message
which failed. Measured:

    ok: a directory reports its modification time
    ok: setting a directory's modification time is refused, as EISDIR

Reading resolves a directory perfectly well. Setting does not, because
`kal_fs_set_modified' takes a `kal_file' and openkal has no form of it that takes
a directory or a name, so this port opens the name as a file. A caller holding a
lock directory reads its timestamp to decide staleness and writes it to refresh
the lock, and only the second fails; openkal-linux#13 reported it as the first.

README.md carries the row. The refusal is asserted rather than merely not
exercised, so that the row is contradicted if openkal ever gains the operation.

Measured: 47 observations, 0 failures, and `OPENKAL_MUSL_TRACE=enosys' over the
whole probe now prints nothing at all.
…nux#13

`openkal-linux 0.7.1` and `openkal-macos 0.6.1` correct `kal_timeout_read` and
`kal_timeout_write`, which decoded a borrowed `kal_stream` handle with the
decoder for an owned one and so waited upon the descriptor below the one they
transferred upon.

THIS PORT IS WHERE THAT DEFECT WAS OBSERVED AND IS NOT WHERE IT WAS. libc++
opens a copy's source with `O_NONBLOCK`, `do_read` routes that through
`okm_timed_read`, and `SYS_copy_file_range` is a `do_read`/`do_write` loop, so
every byte of a file copy passed through a wait upon the wrong descriptor.
`EAGAIN` is not in libc++'s fallback list, so `std::filesystem::copy_file`
reported `Resource temporarily unavailable` for two ordinary files. Nothing in
this port needed to change: the route through `O_NONBLOCK` was correct and what
it reached was not.

`.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md`
records the round: what the report contained, what it did not, and the two items
that remain open.

Two of its findings are worth naming here because they change what the next
round should do.

FIRST, THE CRASH THE REPORT CALLS UNFIXED WAS FIXED IN 0.7.0, AND THE BUILD THAT
WAS MEASURED DID NOT CONTAIN IT. The reported `OPENKAL_MUSL_TRACE` list names
operations 88 and 324. Neither can be printed by 0.7.0: `symlink` and
`symlinkat` have cases, `membarrier` has a case of its own whose stated purpose
is that the trace not report it, and `trace_absent` is called from the default
arm and nowhere else. Both numbers are defined for that architecture, so both
cases are live. The measured library was 0.6.0, and since
openkal-llvm-runtime 0.4.0 requires 0.7.0, the runtime was 0.3.1.

SECOND, AND IT IS WHY THE FIRST COULD HAPPEN: A PROGRAM BUILT ON THIS PORT
CANNOT STATE WHICH VERSION OF IT IT HOLDS. The only thing resembling a version a
program can read is `SYS_uname`'s release field, which is the string literal
"0.5.0" and has never moved. A consumer who checks it is told a version, and the
version is wrong. §7 of the document proposes deriving that field from the
package version and having the trace name the version before anything else; it
is not done here, because it is a change to what a program observes and belongs
in a release of its own rather than beside a repin.
…try does not cover

One row of this branch's first run reported

    curl: (35) Recv failure: Connection reset by peer
    ##[error]Process completed with exit code 35

thirteen seconds in, before anything was built. `--retry` covers a transient
HTTP status and a timeout and does not cover a failure of the transport;
`--retry-all-errors` is the flag that does.

The same unprotected fetch is in openkal-linux, openkal-macos, openkal and
openkal-llvm-runtime, eight occurrences in total. They are left for changes of
their own rather than folded in here.
… to this port

The Windows row of the matrix answered

    FAIL: setting a directory's modification time is refused, as EISDIR (errno=13)

and it was right to. This port opens the name as a file, because
`kal_fs_set_modified` takes a `kal_file` and openkal has no form of it taking a
directory or a name. What a backend says about opening a directory as a file is
the backend's: Linux distinguishes a directory and answers
`kal_err_is_directory`; the system with no such distinction answers
`kal_err_permission`, which is EACCES, which is 13.

So the refusal is asserted and the value is reported:

    ok: a directory reports its modification time
    note: refused with errno=21
    ok: setting a directory's modification time is refused

Asserting the value would have made an observation about one implementation
while reading as an observation about the port -- which is the same mistake, in
a smaller place, as the one this round exists to fix. README.md's row now names
both values and says which is measured where, and the document records the
correction rather than being edited to look as if it had been right.

Measured after removing `target/` and building once: 47 observations, 0
failures. The first run of this was made against a stale artefact --- two
fingerprint directories existed, because the repin changed a dependency version,
and `find | head -1` answered with the older one. `tools/one-artifact.sh` exists
for exactly that and refuses rather than guessing; it is what should have been
used, and it is what produced the reading above.
@Sunrisepeak
Sunrisepeak merged commit 4e7f0ac into main Aug 29, 2026
6 checks passed
@Sunrisepeak
Sunrisepeak deleted the fix/truncate-and-the-timeout-repin branch August 29, 2026 18:31
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