musl 1.2.5 redirected onto openkal.
The specification claims that porting one C library causes the software above it to run on every implementation. This package is that port, and it exists so that the claim can be checked rather than repeated.
[dependencies]
openkal-musl = "0.16.0"Engine floor (mcpp 2026.9.18.3): this version of this package declares
[c-abi](presents, data-model, wchar, builtins) and the engine realises it per compile unit. The probe that verifies the declaration matches what the compiler actually emitted needs the host-macro strip (-U_WIN32etc. on Windows hosts) and the freestanding-fno-short-wchardiscipline — both shipped inmcpp 2026.9.18.3. Older engines silently misbuild this package on Windows × any target: the probe sees host contamination and reports mismatches the package does not have. Upgrade:xlings install mcpp --force.
It names no implementation and no platform: a C library is the one consumer that must know which implementation it needs, so it declares that itself.
That line is for a C program. A C++ program names the runtime instead, and naming both is an error rather than a redundancy.
openkal-llvm-runtime supplies libc++, libc++abi and libunwind configured for
this package, and it pins the version of this package exactly — a version
requirement here does not float up. So a program that names both gets neither:
error: dependency 'mcpplibs.openkal-musl' has irreconcilable versions:
'0.10.0' requested by '<the program>'
'0.9.0' requested by 'mcpplibs.openkal-llvm-runtime@0.5.0'
⇒ A C++ program writes one line, and the C library arrives with it:
[dependencies]
openkal-llvm-runtime = "0.12.0" # carries openkal-musl 0.16.0Recorded here because it was got wrong by the people who maintain this package: the consumer who reported openkal-linux#13 builds C++, and was told to change the line above rather than this one. The version table below is what a consumer needs to answer it without asking.
| this package | is carried by |
|---|---|
| 0.16.0 | openkal-llvm-runtime = "0.12.0" |
| 0.15.0 | openkal-llvm-runtime = "0.11.0" |
| 0.14.0 | openkal-llvm-runtime = "0.10.0" |
| 0.13.5 | openkal-llvm-runtime = "0.9.6", "0.9.7" |
| 0.13.4 | openkal-llvm-runtime = "0.9.5" |
| 0.13.3 | openkal-llvm-runtime = "0.9.4" |
| 0.13.2 | openkal-llvm-runtime = "0.9.3" |
| 0.13.1 | openkal-llvm-runtime = "0.9.2" |
| 0.13.0 | openkal-llvm-runtime = "0.9.0" |
| 0.12.0 | openkal-llvm-runtime = "0.8.0" |
| 0.11.0 | openkal-llvm-runtime = "0.7.0" |
| 0.10.0 | openkal-llvm-runtime = "0.6.0" |
| 0.9.0 | openkal-llvm-runtime = "0.5.0" |
| 0.7.0 | openkal-llvm-runtime = "0.4.0" |
| 0.6.0 | openkal-llvm-runtime = "0.3.1" |
One further line is required for a C program, and the reason is worth stating because it is not about openkal:
[build]
cxx_runtime = "host-coupled"The build tool decides whether to embed a C++ runtime into the program, and on one of the three systems its answer is to embed one and to run that runtime's initialiser first --- before anything else in the image, including the entry point. A program above this package carries no other runtime, so the C library has not started at that moment; the embedded initialiser reaches it through a guarded static and a mutex and finds nothing there, and the report names the dereference rather than the ordering. `host-coupled' says that the build tool embeds nothing: the system's own C++ runtime is named instead, and its initialisers run inside it, on the facilities it was built against.
The decision belongs to the program because the build tool reads it from the program. A dependency that declared it would be declaring it for itself.
A C library is the one layer that knows what a program built above it will
see: whether _WIN32 is true, whether a long holds a pointer, how wide a
L"..." literal is. Since 0.15.0 this package states that environment
rather than leaving mcpp's target triple to imply it, in the manifest's
[c-abi] block:
[c-abi]
presents = "posix"
data-model = "arch-default"
wchar = 32
builtins = "iso"presents = "posix" means __unix__ is defined and _WIN32 and
__MINGW32__ are not, on every target this package builds for, Windows
included. data-model = "arch-default" means musl's own answer for the
architecture — LP64 on every 64-bit target here. wchar = 32 is musl's own
wchar_t. builtins = "iso" states that the compiler must not assume a
platform C library is present beneath this one; this package is the whole
of what a program built on it calls.
| target | machine code | data model | wchar_t |
_WIN32 |
|---|---|---|---|---|
| Linux, macOS (x86_64, aarch64) | native | LP64 | 32-bit | not defined |
| bare metal (riscv64) | native | LP64 | 32-bit | not defined |
| Windows (x86_64) | PE, Win64, SEH — unchanged | LP64 | 32-bit | not defined |
Before 0.15.0, Windows was the one target where this table's last three
columns disagreed with the rest — LLP64 and a 16-bit wchar_t, the data
model and wide-character width the platform's own C runtime uses, which is
not the one musl or this port's other two targets use. musl/PATCHES.md
carried four patches to make that combination survive, and one of the costs
was stated in as many words: a program above this package that wrote
L"..." failed to compile on Windows, deliberately, because narrowing
musl's wchar_t to match would have converted a code point above U+FFFF to
the wrong value with nothing reporting it.
That row is gone, not patched around. This package now presents the
same environment on every target, Windows included, and the four patches
that existed only to make the old presentation survive are removed —
musl/PATCHES.md records what each did and why removing it is correct
rather than merely convenient. A wide-character literal compiles on Windows
now, as it does everywhere else, and a code point above U+FFFF round-trips
through it; examples/c-abi asserts this, together with the rest of the
table above, and runs on every CI row, the Windows one included.
Two limits remain, and neither is about the C environment table above.
Name resolution on Windows is still refused rather than guessed — see "on
Windows" among the absent facilities below — because it depends on a
nameserver list this port's filesystem layer cannot reach there, which
presents = "posix" does not change. And a C program that names this
package directly, without also naming openkal-llvm-runtime, still fails
to link on x86_64-windows-gnu for want of three compiler-rt builtins —
see the same table — because this package carries no builtins archive of
its own; that is a question builtins = "iso" states rather than answers,
and the answer is still openkal-llvm-runtime.
The engine does not trust this block: it compiles a probe translation unit
with the final flags for the target and checks sizeof(long),
__SIZEOF_WCHAR_T__ and whether _WIN32 is defined against what is
declared here, and refuses the build if they disagree.
On Windows, that declaration needs a Clang-based toolchain, and GCC/MinGW
cannot realize it, on any version, with no flag that changes this. Stating
LP64 and no _WIN32 for x86_64-windows-* is not merely a header choice:
mcpp realizes it by compiling with a substituted target triple (clang --target=x86_64-w64-windows-gnu … --target=x86_64-pc-cygwin -fno-short-wchar,
in that order — the object format and calling convention come from the
first, the environment a translation unit sees from the second), which is a
mechanism only Clang's driver has. Plain x86_64-w64-mingw32-gcc has no
equivalent: its long is 32 bits on this target unconditionally —
$ x86_64-w64-mingw32-gcc -dM -E -x c /dev/null | grep __SIZEOF_LONG__
#define __SIZEOF_LONG__ 4— because that is the Windows ABI itself, not a default GCC happens to
choose and a flag could override. A build that declares LP64 and hands GCC
that compiler gets a uint64_t that is actually 32 bits wide, silently,
everywhere this port's own sources or a program above it assume otherwise.
This is why CI carries no "windows, gcc" row: it would not be testing a
weaker configuration of this package, it would be testing a toolchain this
package's own declared environment cannot be realized on at all. A program
that builds this package for Windows needs [toolchain] default = "llvm@<version>" (or the target-scoped form), and gets a compile-time
refusal from mcpp — not a silent 32-bit long — if it does not have one.
musl reaches its kernel through seven inline functions declared once per architecture. Replacing that one header is the whole of the redirection; the 1345 sources that make up the library are compiled unmodified.
musl/PATCHES.md lists the whole of what is not unmodified: five patched
lines, all of one kind — a machine word carried through a variable declared
long, which is not a machine word on one of the three targets — and twelve
replaced sources. Five of the twelve are replaced for the same reason: each
reads the shape of one particular environment rather than asking a kernel for
something.
| Source | What it reads directly | What replaces it |
|---|---|---|
src/env/__libc_start_main.c |
the auxiliary vector Linux leaves on the initial stack | the arguments and named values, from openkal.env |
src/env/__init_tls.c |
the program's own ELF headers | nothing: openkal reports that a started context already observes thread-local storage |
src/thread/__set_thread_area.c |
the instruction that installs a thread pointer | an assignment to one variable |
src/thread/clone.c |
the system call that creates a thread | kal_task_start |
src/process/posix_spawn.c |
duplication of the calling image, then replacement | kal_process_spawn, which is the composite |
The other four were found by running rather than by reading: two carry a machine
word through a long, one refuses a working directory that does not begin with
a separator — correct on every system musl was written for, and not on one that
writes a volume first — and one walks the program headers of an image in a format
two of the three targets do not have.
The tenth is replaced because the fifth was. src/process/posix_spawnp.c does
not search a PATH itself: it hands __execvpe to posix_spawn to be run in
the duplicate, and this port has no duplicate to run it in, so the field was
read by nobody and a name without a separator was taken as a path. It reported
success for a program it had not started.
Four names have no C in musl to fall back on — setjmp, longjmp,
sigsetjmp and the cancellable system-call sequence. The first three are
supplied by one assembly file that assembles for every object format; the
fourth is a C function, because what it did that C could not do was issue a
system call from a known instruction, and there is no such instruction here.
The measurement that found those four was made by reading the contents of
musl's placeholder sources rather than checking that they exist. musl ships a
zero-byte .c for each so that its build system does not fail, so a count of
files answers "all thirty have a C version" and a count of bytes answers
"twenty-six do".
Everything a C program ordinarily uses works, and examples/posix asserts it:
formatted output, the environment, an absolute working directory, files with
creation, positioning, truncation, enquiry and removal, buffered streams,
appending, directories with enumeration, renaming, both clocks, execution
contexts with contended mutexes, allocation and reallocation, and starting
another program and awaiting it.
Sockets, datagrams, readiness and the duplication of the calling image were in
the table below until openkal 0.8, and they are not there now. What changed is
the specification: openkal.net, openkal.datagram, openkal.timeout and
openkal.space gave this port the atoms it had been missing, and each of the
four is now composed above them rather than refused. examples/net and
examples/subprocess assert them, written against POSIX and naming no openkal
symbol.
Every one of those four depends on what is beneath. Clause 3 permits an
implementation to provide an interface in whole or not at all, and the four
interfaces are optional. Where a backend declines one, the routes that use it
report ENOSYS — the port takes a weak reference and tests it before calling,
so a program that never opens a socket links and runs above a backend that has
no network at all.
The following are absent, and each is refused rather than quietly accepted, because a facility that reports success and does nothing is the one kind of answer that leaves a program wrong without telling it.
Since 0.17.0 this table has an executor. [c-abi-absent] in mcpp.toml
states each facility and the SHAPE in which its absence reaches a program ---
link (the definition is not in the archive), enosys (it is, and reports
that it cannot act), accepted-no-effect (the call succeeds and part of what
it asked for is not done) --- and tools/check-absent.sh asserts every row
against the objects this package builds, in the direction that row states. A
link name that turned out to be defined, or an enosys name that turned out
to be missing, fails the build.
Prose alone had no executor, and it was contradicted once: before 0.16.0
SIG_IGN was accepted for every signal and installed for none, so a program
that asked not to be ended by the interrupt keystroke was told it had
succeeded and was ended by it. accepted-no-effect is that shape, and it is
named so that "how many of these are there" is a question with an answer.
link is the shape openkal's own capability model requires of an
implementation (SPEC 0.14 clause 6.1, which calls a run-time report of
unsupportedness a defect). The other two are departures from it, and a reader
counting them is reading the cost of presenting POSIX above an interface that
does not carry all of it.
Two things this table says and the manifest cannot, stated rather than rounded off.
A facility narrower than a name. "A mode given at creation" is about an
ARGUMENT of open and mkdir, not about those calls: they do what they are
for and the mode is what is not applied. An entry keyed on open would say the
call is absent, which is false and worse than no entry.
An absence that varies by target. fork is composed here from
openkal.space, and the reference to it is weak: a backend that provides the
interface gets a working fork, and one that does not gets ENOSYS.
openkal-linux provides it; openkal-windows declines it, and its README gives
the reason --- constructing the copy out of CreateProcessW would be present,
would look like the operation, and would not produce a copy of the caller.
Every row of [c-abi-absent] is unconditional, so fork written as enosys
would be false on Linux and omitting it is silent on Windows; silence is the
lesser of the two, and the row returns when the schema carries targets.
| Absent | What a program observes | Why |
|---|---|---|
| signal handlers | sigaction reports ENOSYS for any handler other than the default or ignore. Since 0.16.0 a disposition is accepted only where it is the one already in effect: SIG_DFL succeeds for every signal but SIGPIPE, SIG_IGN succeeds for SIGPIPE alone, and the enquiry reports SIG_IGN for SIGPIPE rather than a zeroed record |
openkal has no asynchronous delivery. A handler that was accepted and could never run would be silently wrong; masking, which has nothing to mask, succeeds. Until 0.16.0 SIG_IGN was accepted for every signal and installed for none, so a program that asked not to be ended by the interrupt keystroke was told it had succeeded and was ended by it. SIGPIPE is the one disposition that is not the default, and not by accident: openkal requires a write to a stream whose far end is gone to report the condition rather than end the program, so an implementation beneath has already arranged that the signal does nothing. |
| a terminal's whole state | tcgetattr and tcsetattr carry line assembly, the echo, and whether the environment reserves keystrokes — the three positions openkal names. Since 0.16.0 they reach the terminal: TCGETS, TCSETS/TCSETSW/TCSETSF and TIOCGWINSZ are performed through openkal.terminal, so cfmakeraw followed by tcsetattr puts the terminal into raw mode and the interrupt keystroke arrives as the byte 0x03. What a program cannot change is everything the structure carries that openkal does not name: output post-processing (OPOST), the line speed, the control characters, VMIN/VTIME, and the draining the W and F forms ask for. A tcsetattr that alters one of them is accepted and that part has no effect --- measurably: a program in raw mode that writes a newline still gets a carriage return before it, where the same program above the system's own C library does not; tcgetattr reports the composition port/src/okm_syscall.c states |
openkal's mode word has three positions and struct termios has four flag words and twenty characters. The three are the ones a program needs in order to read keystrokes; the rest are either the terminal's own (the speed, the characters) or output-side, and openkal names none of them. Until 0.16.0 TCGETS and TIOCGWINSZ reported success and wrote nothing into the caller's structure while TCSETS was refused, which is #36. A program that wants a read to give up asks kal_timeout_read, which is where openkal states a bound upon waiting. |
| memory protection | mprotect reports ENOSYS |
openkal has no operation upon a mapping's protection. musl asks for a guard page below a thread's stack and proceeds without one when told this, so the honest answer is also the one it is prepared for. |
| out-of-band data | MSG_OOB, MSG_PEEK, and POLLPRI are never reported and recv refuses the flags |
openkal's transfer operations move bytes and have no second channel and no non-destructive read. |
| readiness sets | epoll is not built at all, so the link names it |
a set held by the environment is a facility of one kernel rather than a capability. poll and select ask each descriptor in turn, which is what an interface without a set permits. |
answered since 0.7.0 — symlink, readlink, and stat/lstat telling the two questions apart |
openkal 0.9 carries kal_fs_link_create and kal_fs_link_read as operations of openkal.fs, and kal_fs_props takes the directory, so this port asks whether the volume has such nodes before it uses them. Where it does not, the refusal is what the enquiry already said. |
|
| permission bits | chmod/fchmodat report ENOSYS for a request whose read or write bits differ from what stat already reports for the node; since 0.14.0 a request that changes only the execute bits, to the one shape a class with a read bit can be given — all three set, or all three clear — succeeds and round-trips through stat. A request equal to the mode already reported always succeeds, even where the volume does not claim KAL_FS_PROP_EXECUTABLE, because nothing is then being asked of it. fchmod still reports ENOSYS unconditionally: this port keeps no name for an open file to hand kal_fs_set_executable_at |
kal_node_info carries writable — one boolean, not a mode word — so the read and write bits this port can report are always the same across the three classes, and a request for anything else would report success for a mode stat does not then show, which is the shape this port exists to avoid. openkal 0.13 added kal_fs_set_executable_at for exactly the one bit this port can honestly change; SPEC.md clause 11 entry 6 states why it is a property of the node and not a permission. |
| the identity of a node | st_dev and st_ino are the implementation's answer where it has one, and zero for both where it has none |
They were the constants 0 and 1, so every file compared equal to every other: std::filesystem::equivalent on two separately created files answered true with no error. openkal 0.9 carries an identity and reports whether it knows one; a caller must not read two zeroes as sameness, which is why nothing is invented for an implementation that cannot distinguish nodes. |
| ownership | chown reports ENOSYS; stat reports 1000 for both |
as the row above: a capability-oriented environment has no principal for an owner to name. |
| a mode given at creation | open(…, O_CREAT, 0600) and mkdir(path, 0700) succeed and stat afterwards reports 0666 and 0777 |
the row above, in the one place where it does not read as a refusal. openkal opens a file for a purpose and not for an audience, so the argument has nowhere to go. Refusing every mode but the one stat will report would refuse nearly every program; what a caller can rely on instead is stated below. |
| entropy | getrandom reports ENOSYS where the backend declines openkal.random |
openkal has no source of one to require, and this port does not invent one. The allocator's cookie and the stack canary are derived from the clock and from an address; neither is a security property here. |
| a signal delivered anywhere | raise and kill perform a signal's default action and nothing else: terminating signals end the program, ignored ones succeed, stopping ones report ENOSYS, and musl's own three (32, 33, 34) are refused, so pthread_cancel reports ENOSYS |
there is no delivery, so there is no handler to reach; what remains of a signal is what it does when no handler exists. abort reaches kal_abort, which raises the signal on Linux and ends with a distinguished status elsewhere — a parent can tell an abnormal end from an ordinary one on every system. |
| an immediate answer about a started program | waitpid(…, WNOHANG) returns without the program having finished, but may wait up to one polling interval of the implementation beneath (one millisecond on Linux) |
kal_timeout_wait_process takes a bound and openkal spells "no bound" as zero, so a caller that does not want to wait asks for the smallest bound there is. An environment rounds a bound up to what its clock can distinguish; a bound shorter than the clock is a promise no environment can keep. |
| closing a standard stream in a program being started | posix_spawn_file_actions_addclose(&fa, 0…2) makes the spawn report ENOSYS; above position two it is performed, because nothing there is inherited |
openkal has no value meaning "no stream", and the value that looks like one — zero — means the opposite: the stream the caller has. Accepting the action and not performing it would hand a program the standard input its caller had just taken away. |
| starting a program upon a stream whose handle is zero | a caller that redirects its output onto its own standard input and then starts a program gets ENOSYS |
kal_spawn_streams reserves zero for inheritance and kal_stream reserves nothing, so an implementation whose streams are the environment's own descriptors hands out zero for standard input. The two agree at position zero and cannot be told apart anywhere else. Reported upstream; refused here rather than answered wrongly. |
answered since 0.9.0. uname's release field is this package's version, and OPENKAL_MUSL_TRACE=enosys names it on the error stream once per process before the program runs |
It was the string literal 0.5.0 through every release after 0.5.0, so a program that asked was not left without an answer -- it was given a false one. It therefore MOVES AT EVERY RELEASE: nothing here or in musl reads it (gethostname and getdomainname are musl's only consumers of uname and both read nodename), but a program above it that compares the field against a fixed string will see it change. sysname is openkal and not Linux, so nothing can have been reading it as a kernel version. |
|
answered everywhere since 0.11.0. 0.10.0 answered it only where the implementation could open a directory --- Linux and macOS could and Windows could not, because its kal_fs_open names FILE_NON_DIRECTORY_FILE. Reading it was never affected |
0.10.0 reached a directory's time by opening the directory for READING and stamping that, which worked and was outside anything fs.h stated --- there was no route to a directory's time at all. openkal 0.10 added kal_fs_set_modified_at, which takes a NAME, and every implementation answers it: the Windows one opens for the attribute alone, which reaches a directory where opening a FILE cannot. The divergence this row recorded was caused by a missing declaration, and it went away when the declaration arrived. |
|
fcntl(F_SETLK) and F_SETLKW take a real lock since 0.11.0. They used to answer 0 and do nothing, so two programs took one exclusive lock and both were told they had it. The holder is the open file, not the program: a SECOND open file of one name is refused here, where the older process-held form would have granted it and a library that opened one file twice would have destroyed its own lock. F_GETLK is still refused, and flock has no case |
0.10.0 refused all three and said the refusal was temporary in a way the permission row is not — every environment beneath openkal can lock a byte range and what was missing was a word. openkal 0.10 added kal_fs_lock with KAL_FS_PROP_LOCKS, and this is what that record said would happen. F_GETLK remains refused because 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. |
|
| whether a file may be executed | Answered on both halves since openkal 0.13. access(path, X_OK) and stat's execute bits follow KAL_INFO_EXECUTABLE when the enquiry reports the position filled (a file, on a volume that claims KAL_FS_PROP_EXECUTABLE, since 0.14.0 also settable — see the row above). Where it does not, the two read the absence differently, as they did before 0.14.0: stat reports a file without execute bits, and access(X_OK) answers yes, so a program that asks before starting a name still attempts the start and is told the reason. A directory is reported traversable by both, and a chmod that would change a directory's execute bits reports ENOSYS. Starting a name that exists and cannot be run no longer ends the caller at 127 either: posix_spawn reports a reason and returns no pid — ENOENT, EACCES, EISDIR, and, since openkal 0.13's kal_err_not_program, ENOEXEC for a name that is there, may be started, and is not in a form this environment recognises |
kal_node_info carried writable and no other permission before 0.13, so "it is there" was the whole of what this port could answer, and a start that could not happen was folded into kal_err_io — reporting a device failure when none occurred, and giving a C library above no way to map the failure back to ENOEXEC, which every environment this port targets has natively. Versioned, because the three implementations did not move together. openkal-linux has reported the reason for a start that did not happen since 0.8.0, and reports kal_err_not_program for it since 0.13.0. openkal-macos reported kal_ok and a handle for every name that could not be started — including one that does not exist at all — until 0.10.0; a program above an older openkal-macos still meets 127, indistinguishable from a program that ran and returned that status. openkal-windows fails synchronously in the caller (CreateProcessW needs no pipeline for it) and maps the condition to kal_err_not_program since 0.8.0. |
| descriptors above 2 crossing into a started program | a started program receives standard input, output and error and nothing else; a non-close-on-exec descriptor 4 is not there, and fcntl(F_SETFD, 0) upon one therefore changes nothing |
kal_spawn_streams has exactly three positions and openkal has no general form for placing a stream at position n. posix_spawn_file_actions_adddup2 above position two is already refused rather than accepted, so the two agree; only implicit inheritance is lost. A general form has been asked for. |
answered since 0.12.0, together with posix_spawn_file_actions_addchdir_np and addfchdir_np. openkal 0.11 gave kal_spawn a second directory — base resolves the program's NAME, work is where it RUNS — and a copy that called chdir carries its own into the execve it then performs. The rest of this row is what it said before |
this one was silent, and it is the one a consumer is most likely to meet: the call reported success, the caller's own paths followed it, and only the started program disagreed. kal_process_spawn's base is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carried one. It could not be composed here either: chdir in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. The route was a declaration rather than a composition, which is the same shape as the directory-time row above. |
|
answered since 0.12.0 for the form that works, which is posix_spawn with POSIX_SPAWN_SETPGROUP and a zero group: the started program forms a unit, what it starts inherits the unit, and kill(-n) upon the identifier posix_spawn returned reaches all of it — including a shell's backgrounded work, which is the case a timeout has to reach. setpgid(0, 0) in this program forms a unit too. Naming another program's group is still EPERM, and setsid is still EPERM |
openkal 0.11 gave kal_spawn a job, which is where a unit is established: the caller says which unit, and the implementation performs the placement. What this costs is stated in the row below, because it is one composition and not a general gap. |
|
fork(); setpgid(0, 0); exec … |
the copy forms a unit and the original cannot name it, so kill(-n) from the original reports ESRCH. Use posix_spawn with POSIX_SPAWN_SETPGROUP, which is the row above |
this is a property of the interface and not a shortfall of this port. A unit is named by a handle, openkal handles are built from an index into the holder's own table (clause 6.7), and nothing conveys one across a spawn (clause 11 entry 9) — so the unit the copy forms is the copy's, and the original never learns of it. 0.12.0 reached for the nearest unit it could name, which was its own, and every negative identifier that matched no child therefore named the caller's group: an enquiry about a unit that did not exist was answered yes, and a signal aimed at one ended the caller together with everything it led. 0.13.0 answers ESRCH and examples/subprocess checks it. openkal 0.12 records the whole of it in clause 11 entry 9, so the next implementation meets it in the specification. |
answered since 0.11.0. sched_getaffinity, and therefore std::thread::hardware_concurrency() and sysconf(_SC_NPROCESSORS_ONLN), report the real count |
it used to be silent: a program sizing a pool of workers got one worker and no error. openkal 0.10 added kal_task_parallelism. Zero from that enquiry means cannot say and is reported here as a refusal rather than as a bitmap of one processor, because musl would read the latter as a fact this port had invented. |
|
| hard links, named pipes, a bidirectional pair | link (create_hard_link), mkfifo and socketpair report ENOSYS. statvfs (std::filesystem::space) is answered since 0.11.0 |
openkal has no operation for the first three. kal_fs_link_create makes a node whose content is a name — a symbolic link — and there is no hard link; kal_process_channel is a pipe in one direction, so a bidirectional pair is not one of them. Volume capacity was in this row until openkal 0.10 added kal_fs_capacity. |
| an alternate signal stack | sigaltstack reports ENOSYS since 0.10.0 |
it used to report success and install nothing, and the enquiry that would have caught it answered 0 with a zeroed record. There are no signals here, so there is nothing for such a stack to be. |
| one C runtime and one C++ runtime per image | a static library compiled against the platform's own C runtime, or an object that runtime owns crossing the boundary — a FILE*, memory one side allocates and the other frees, errno — is not supported. Only handles and values may cross into a platform's own system interfaces |
this package supplies the whole of a program's C library; a second one linked in beside it duplicates every piece of global state each keeps (the allocator's own arena, errno, locale) and the two do not agree about any of it. A vendor SDK distributed only as a static library built against another C runtime is n/a on this target — a design consequence and not a defect. openkal's own README states the same rule for every implementation built above it; the ecosystem design record (.agents/docs/2026-09-17-openkal-ecosystem-cross-repo-design.md, R1) states why it binds a package and not the specification. |
| a context a platform library creates | code reached from it has no C library state — no errno, no locale, no pthread_self — and must not rely on any, because none of it exists until kal_task_start builds it, and such a context was not built by it |
musl's per-context state lives in thread-local storage this port's own thread start fills in. A callback from a platform's own thread pool, from COM, or from a framework's own callback thread (Windows, macOS's GCD) arrives on a context this library never started. A package reached this way confines such a callback to work that does not touch this library, or hands the work to a context of its own before it does (R2 of the design record above). |
| resolving a host name by name, on Windows | getaddrinfo reports EAI_AGAIN; a numeric address is unaffected |
musl's resolver reads /etc/resolv.conf for a nameserver list, through this port's own filesystem layer. openkal defines no name resolution (SPEC.md clause 3.4, clause 11 item 5); the nameserver list is the environment's, and on Windows it is kept where this port cannot read it — there is no /etc/resolv.conf for any layer here to find. Answering with a guessed nameserver would be silently wrong, so this reports that the name could not be resolved rather than inventing one. A program that needs names on Windows supplies its own resolv.conf in a directory it was given (a preopen), or resolves by another means. Measured under Wine, openkal-windows 0.7.4 and this package's 0.13.5: getaddrinfo("httpbin.org") returns -3 ("Try again") and no resolv.conf is present; the same program on Linux resolves the name and connects. |
the compiler-rt builtins a C program needs, on x86_64-windows-gnu |
a C program that names this package directly fails to link: ___chkstk_ms, __muldc3 and __mulxc3 are undefined |
this package supplies no C++ runtime and carries no builtins archive of its own. openkal-llvm-runtime builds compiler-rt for the target, and a C++ program names it for the runtime and receives these three along with everything else it provides; a C program that names only this package does not. Measured with mcpp 2026.9.17.2, including the fix for mcpp#662. Not addressed here: a C program on this target names openkal-llvm-runtime as well, or supplies the three symbols itself. |
What carries confinement here, since a mode word does not. A program that
writes "only I may read this" as a mode is stating it in a vocabulary this
environment does not have. What it does have is stronger and is not the
program's to weaken: a program reaches only the directories the environment
supplied it, and port/src/okm_fd.c states the rule — confinement is a property
of what was supplied, not of the program's cooperation. A caller with that
requirement expresses it by being started with fewer directories.
The permission row is a decision and not an omission. The alternative was
to ask the specification for a permission operation. It was declined: a FAT
volume, a UEFI system partition and a Windows access-control list do not share a
model, so an operation upon permissions is one that some resources of the
interface can never satisfy — which is what clause 6.4 excludes. Refusing here
and stating why is the answer; .agents/docs in the specification's repository
records the reasoning.
And what to write instead, which a refusal by itself does not tell you.
A program that means only I may read this is stating it in a vocabulary this
environment does not have: a permission presupposes an identity, and openkal
models no identity for one to name. This is not a gap peculiar to openkal — it is
where the whole capability family sits. WASI's filestat carries no mode, no
uid and no gid, and path_open takes no mode; what WASI does have,
fs_rights_base, attaches to a handle and not to a file, and its analogue
here is the flag word open already takes. Add a mode to that and you have
borrowed POSIX's shape without POSIX's subject.
openkal's clause 11 entry 6 gives the three answers, and they sort by whom you are defending against rather than by mechanism:
| defending against | the answer |
|---|---|
| another part of the same program | the capability already does it — a handle not given cannot be reached |
| another user of the machine | the party that STARTS the program, through the preopens it supplies and withholds. In a capability system "make this file private" is not an operation upon the file; it is a property of the directory you were granted |
| a location you do not trust | encrypt the contents — where the location is untrusted no access control holds anyway |
The consequence for porting. Code that hardens itself with chmod(0600)
after creating a file — asking to change who may READ or WRITE it — should not
treat the refusal as a failure to create; the directory it was given is either
private already or was never going to be made so from inside.
access(X_OK) is a different question, and openkal 0.13 answers it
separately from the paragraph above. Whether a node may be started
presupposes no principal, the way chmod(0600) does: a volume that records it
records it once for the node, not once per class of caller, so it is a
property rather than a permission — SPEC.md clause 11 entry 6 states the
argument and why it holds for this position and fails for a mode word. That is
what KAL_INFO_EXECUTABLE and kal_fs_set_executable_at answer, and this
port's rows above state what a program built on it observes.
Two further boundaries are properties of the arrangement rather than omissions.
Names that ascend are resolved here, not by the environment. openkal refuses
a name containing .., because a program able to ascend from the directory it
was given would not be confined by having been given it. /a/b/../c is
therefore reduced to /a/c before openkal sees it, which is what the program
means in every case except one that passes through a symbolic link.
O_NOFOLLOW is answered by an enquiry, not by an opening. openkal states
that opening resolves and offers no form that declines to, deliberately: a
program that opens a link in order to read its bytes is asking what
kal_fs_link_read answers. So this port asks kal_fs_info with
KAL_FS_NO_RESOLVE first and reports ELOOP when the name is a link, which is
what POSIX says and what a caller passing the flag is distinguishing. Answering
ENOENT instead — which is what resolving a link to an absent target produces —
is a different answer to a different question, and libc++'s remove_all reads it
as "the entry has already gone" and leaves the tree standing.
The tables are bounded. A program may hold 1024 descriptors and 512 open descriptions; beyond that it is told so. Allocating the tables instead would place them on the allocator, and the allocator obtains its memory through them.
And a program may have started 256 programs it has not waited for. An
entry is taken when a program is started and released when it is waited for,
which is what a process table is; a program that never waits holds entries for
ever, and the next start reports EAGAIN — which is what POSIX says fork
does when the table is full. This bound is stated here because it was not, and
a caller that met it saw a failure on an operation with no evident relation to
the ones that caused it: measured, the sixty-fifth posix_spawn of a program
that waited for none, and of one that polled each once with WNOHANG and did
not come back.
ENOSYS says that a facility is not here. It does not say which one, and until
0.6.0 the only way to find out was to read port/src/okm_syscall.c — which is
not a thing a consumer of a C library should have to do, and two rounds of
openkal-linux#13 were
spent on exactly that question.
OPENKAL_MUSL_TRACE=enosys ./your-program
Each operation the dispatcher has no case for is then named on the standard error stream, once, whatever the number of attempts:
openkal-musl 0.10.0
openkal-musl: no operation for system call 266
The first line is the version, and it is printed whether or not anything is missing. That is the whole reason it exists. Before 0.9.0 a run in which nothing was refused printed nothing at all, and three situations were then indistinguishable: the version is right and no operation is absent, the variable did not take effect, or this is not the binary the reader thinks it is. Two rounds of openkal-linux#13 were answered against the wrong version because of it.
A report pasted into an issue therefore carries its own provenance, and one process contributes one such line — a program that starts another produces one for each, and they must agree.
Four properties, each of them asserted in continuous integration because each of the corresponding failures is quiet:
- Nothing is reported unless the variable is set. A diagnostic that appears by itself is one every program above this library has to explain to its users.
- Once per operation. A program that retries in a loop would otherwise bury the report in copies of itself, and a reader counting lines would conclude it happened once.
- Only the operations that have no case.
mprotectandrt_sigreturnanswerENOSYSfrom cases of their own, each a decision with a reason recorded beside it. Reporting those would name a facility as missing that this port deliberately does not have, which is a different sentence. - The version named is the one in
mcpp.toml. It is read from the manifest bybuild.mcpprather than written out a second time, and the workflow compares the printed line against the manifest. A version stated in two places agrees until one of them is edited.
The report is written to the stream directly rather than through this library's own output, because what failed may be the operation that output was about to perform.
Two structures live in port/ that the specification forbids an
implementation to have: a table of descriptors, and a resolver of names.
That is the arrangement rather than a violation of it. Clause 7.1 excludes them from an implementation because an implementation exists once per environment, and a table required of every implementation is a compatibility layer written as many times as there are environments. Here there is one of each, above the boundary, shared by every environment — which is what the specification's decomposition is for.
The measurement the specification asks for is the size of what exists only to bridge a difference in shape:
| lines | |
|---|---|
the system-call correspondence (okm_syscall.c) |
1703 |
sockets and datagrams (okm_net.c) |
788 |
descriptors and name resolution (okm_fd.c) |
512 |
setjmp and its relatives (okm_setjmp.S) |
316 |
how a second name is made (port/include/features.h) |
288 |
startup and the thread pointer (okm_start.c) |
282 |
readiness and bounded transfer (okm_poll.c) |
256 |
execution contexts and the suspension primitive (okm_thread.c) |
203 |
starting another program (okm_spawn.c) |
372 |
where per-context state is kept (okm_context.c) |
162 |
duplicating the calling image (okm_fork.c) |
166 |
what two object formats do not provide (okm_format.c) |
105 |
| mapping, the working directory, program headers, the architecture seams, the one file each of two object formats needs | 398 |
| total | 5551 |
Against 1345 musl sources compiled unmodified. The ratio is the measurement: if openkal's decomposition were wrong, the port layer would be where the difference in shape accumulated, and it would grow rather than the library above it shrinking. Between the first system and the third it grew by 800 lines, and every one of those is about an object format or a naming convention rather than about a kernel — which is the shape of the result rather than a qualification of it.
It grew by 2294 more when the socket, datagram, readiness and image-copying
routes were added, and that number deserves a reading rather than a footnote.
Roughly half of it is comment; of the code, the largest single piece is the
state machine in okm_net.c, and what that machine bridges is one difference in
shape: BSD makes a socket first and decides what it is afterwards, and openkal
has no object between "nothing" and "a connection". The rest is the read-ahead
that answers a readiness enquiry, which exists because openkal deliberately has
no operation reporting whether a transfer would proceed (clause 6.3).
Both are shape and neither is environment. They are written once here and are what every implementation of openkal is spared — which is the property the ratio was measuring in the first place, and the growth does not change its direction.
Corrected in openkal-linux 0.12.0. An earlier version of this section said
that an aarch64 build measured through qemu-user could not start a program of
its own architecture, and that this was a property of the emulator. That was
wrong, and the way it was wrong is worth more than the fact:
- the symptom appeared only on
aarch64, where every foreign binary needs thebinfmt_miscinterpreter, so every start failed at once; - the release before it failed identically, which was read as "pre-existing, therefore not ours";
- a consumer's 108 tests passed on both architectures, which was read as "not reaching users".
⇒ Each of those was true. The conclusion drawn from them was not.
What it actually was. execveat with a directory descriptor and a relative
name gives the kernel the program's name as /dev/fd/<dirfd>/<name>. For an
ordinary executable that spelling never surfaces — the kernel holds the file open
already. For a program that needs an interpreter it does: the kernel starts
the interpreter and hands it that name to open, after the replacement, by which
time a close-on-exec descriptor is gone. The interpreter is told the file does
not exist.
Isolated in twenty lines of ordinary C, with nothing of openkal in it:
dirfd WITH O_CLOEXEC execveat -> ENOENT
dirfd WITHOUT O_CLOEXEC STARTED ok
It was never about architecture. Two kinds of program need an interpreter, and both were refused on every system:
| program | needs an interpreter |
|---|---|
a #! script |
yes — on every architecture, including a plain x86_64 machine |
a binary of another architecture, through binfmt_misc |
yes — which is why aarch64 showed it first |
| an ordinary native executable | no — which is why nothing else failed |
⇒ The emulator only made it visible. A consumer running shell scripts on real
aarch64 hardware — or on x86_64 — met the same refusal. examples/subprocess
now starts a script the shell made executable, and asserts it runs.
examples/wordcount is an ordinary POSIX program whose source mentions nothing
of any of this. Its three counts are compared against the system's own wc,
which is an oracle this package did not produce; a program that merely produced
output would prove only that it produced output.
examples/posix makes 32 assertions, each written so that it can fail. The
three conditions kal_fs_open exists to express are observed by their effect
rather than by a return value: a truncation that did not happen leaves a longer
file, an exclusion that did not happen succeeds, and an append that did not
happen overwrites.
examples/net makes 35 over a listener, a connection and a pair of datagram
endpoints, all on the loopback address, and asks nothing of the network beyond
the machine it runs on.
examples/subprocess starts another program three ways — fork, system,
popen.
Those two say what they expect on the command line rather than inferring
it. --fork requires that duplicating the calling image work; --no-fork
requires that it be refused. An environment whose backend declines
openkal.space is not a failure, and an environment expected to provide it that
quietly does not IS one — a probe that accepted either answer could not tell
them apart, and the interesting failure is exactly the one it could not see.
examples/identifiers compiles rather than runs. It declares hidden, weak
and weak_alias as ordinary identifiers, which a program above this package
could not do until the internal overlay's macros were scoped to the overlay
(mcpplibs/openkal-musl#13). If any of the three is a macro again, the file does
not compile.
| ELF | the reference case. musl gives almost every public name to a definition through a weak alias, and this format has weak aliases |
| PE | measured, with both toolchains: a weak symbol is not a definition there. The 289 aliases are made strongly, and the 46 names musl provides as placeholders for another source to replace are not made at all — which is decided by the name of the placeholder's target, so no site of musl's is edited |
| Mach-O | the compiler refuses the construct outright: aliases are not supported on darwin. The assembler's own directive makes the name instead, and the same placeholder list applies. Two further properties of that format are recorded in musl/PATCHES.md: a definition nothing else in the unit refers to is deleted before the assembler sees the name, and a section with no content has no atom and takes its symbols with it |
Each is recorded in musl/PATCHES.md rather than left to be discovered.
fork is composed here rather than required beneath. openkal.space starts
a context in a copy of the calling address space, and stops there: the started
context begins at a function the caller names, not at the instruction the caller
was executing, because that is what can be stated in a C application binary
interface at all. fork returns twice, so the second half is this port's:
setjmp before the call, longjmp in the copy. The specification's own
space.h describes that composition and says in terms that it belongs above the
line, which is where it now is (port/src/okm_fork.c).
An earlier version of this file said fork was absent and would stay
absent, on the reading that clause 7.1 declines to duplicate an address space
and its execution state. Half of that is right: the clause declines the
pair. openkal.space supplies the first half by itself, and what was
missing was never an atom.
execve is starting a program, waiting for it, and ending with its status.
It is what every environment without the operation does, and two of the three
beneath openkal are such environments.
This paragraph used to add that a caller cannot distinguish it. A caller
can, and saying otherwise is what kept anyone from looking. Three differences
were known and musl/PATCHES.md states each. Two are now answered: a program
that cannot be started (0.10.0 — the name is asked about first, so execvp
can search a PATH; and openkal-linux 0.8.0, which reports an exec that
failed, so a name that exists and cannot be run is refused too), and a kill
that reached the waiting copy rather than the program (0.11.0 — openkal 0.10
added kal_process_spawn_bound, and execve, alone, asks for it). What remains
is the identifier the started program reports.
A program named without a suffix is tried with one environment's suffix second, which is what every C library for that environment does. It is here rather than beneath because openkal is deliberately literal about names: it passes on the name it was given and does not know that a program is a kind of file.
examples/posix |
32 observations, each written so that it can fail |
examples/net |
35 over sockets, datagrams and readiness, on the loopback address |
examples/subprocess |
22 observations: another program started five ways, where its output went in each of them, how an abnormal end is reported, and three refusals checked as refusals |
examples/identifiers |
three names a program above this library may use, asserted by compiling |
examples/wordcount |
the same three counts as the system's own wc |
mcpplibs/sbase |
all 97 suckless base utilities, sources unmodified, 50 comparisons against the system's own tools |
The third is the one the specification's claim is actually tested by. sbase has
no Windows support and does not build on macOS as it stands — three of its tools
include <sys/sysmacros.h>, which is a header of the Linux C libraries, and four
use st_mtim where that system's C library has st_mtimespec. Neither obstacle
is in a kernel; both are the C library, and this package removes them by being
present rather than by being adapted to.
This package was openkal-libc up to version 0.2.0, and the version line
continues rather than restarting: 0.1.0 and 0.2.0 name the earlier package in
this repository's own tags, so a version that restarted at 0.1.0 would give one
tag two meanings. The name changed at 0.3.0 and the numbers do not go backwards.
That package was a probe of
243 lines exporting 14 names — it did the two things the specification places
outside itself, and it was not a C library. It has no separate existence now:
its path resolution is the resolver described above, and its mutex is musl's.
| system | toolchain |
|---|---|
| Linux | gcc, llvm |
| macOS | llvm |
| Windows | gcc, producing PE |
The third toolchain mcpp offers is absent, and the reason is a property of the sources rather than a gap: musl's four remaining assembly definitions are in an object format that toolchain does not assemble, so the question it would answer is not one this package can ask.
The port is Apache-2.0. The vendored musl sources under musl/ are MIT, and
musl/COPYRIGHT is unchanged.