Skip to content

openkal-musl 0.1.0: musl 1.2.5 above openkal, on Linux, macOS and Windows - #1

Merged
Sunrisepeak merged 21 commits into
mainfrom
feat/musl-on-openkal
Aug 20, 2026
Merged

openkal-musl 0.1.0: musl 1.2.5 above openkal, on Linux, macOS and Windows#1
Sunrisepeak merged 21 commits into
mainfrom
feat/musl-on-openkal

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Renames this package from openkal-libc to openkal-musl — the name says which
C library it is — and replaces the hand-written subset it contained with musl
1.2.5 itself, redirected onto openkal.

The shape of the port

musl reaches its environment through one seam: seven inline functions in
arch/<arch>/syscall_arch.h, plus __get_tp in pthread_arch.h. Everything
else in musl is written against those. port/include/ supplies both, so 1682
vendored files are compiled unmodified and reach openkal instead of a kernel.

musl/PATCHES.md lists the whole of what is not unmodified: four 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 nine
replaced sources
, each with the reason it is replaced. Five of the nine read
the shape of one environment directly — the auxiliary vector, the program's own
ELF headers, the instruction that installs a thread pointer, the system call
that creates a thread, and the duplication of the calling image.

Three systems, and what each cost

Linux the reference case; nothing beyond the seam
Windows a weak symbol is not a definition on PE, as both toolchains implement it. Measured, not assumed. The 289 aliases are made strongly, and the 46 names musl provides as placeholders are not made at all — decided by the name of the placeholder's target, so no site is edited
macOS the compiler refuses the construct outright there. The assembler's own directive makes the name instead, and the same placeholder list applies unchanged

Two findings from Windows are worth stating because neither is visible in any
source:

  • A C library cannot keep its per-context state in a thread_local variable.
    On one toolchain such a variable is reached through a helper that allocates,
    and the allocator is what the C library provides. port/src/okm_context.c is
    a table keyed on kal_task_current().
  • Every truncation to long in the dispatcher is a fault waiting on a target
    where a long is narrower than a pointer.

Three operations expressed differently

Recorded in musl/PATCHES.md rather than left to be discovered:

  • fork is absent and stays absent. Duplicating a running image is not
    something every environment can produce, and clause 3.1 declines to simulate
    what cannot be supplied. posix_spawn, and therefore system and popen,
    are supplied.
  • execve is starting a program, waiting for it, and ending with its status.
    A caller cannot distinguish that through this library; there are two images
    where a system with the operation would have one. It is what every environment
    without the operation does, and two of the three beneath openkal are such.
  • A program named without a suffix is tried with one environment's suffix
    second, which is what every C library for that environment does.

What runs

examples/posix 32 observations: an absolute name opened, a variable read, an interval measured, a program started and awaited
examples/wordcount the same counts as the system's own wc
mcpplibs/sbase all 97 suckless base utilities, sources unmodified, 50 comparisons against the system's own tools

Continuous integration runs the first two on Linux (gcc and llvm), macOS (llvm)
and Windows (gcc, PE).

Requires mcpplibs/openkal#1.

mcpplibs and others added 21 commits August 20, 2026 07:59
The package was openkal-libc: 243 lines exporting 14 names, doing the two
things the specification places outside itself. It was not a C library, and the
claim it was written to test - that porting one C library causes the software
above it to run on every implementation - cannot be tested without one.

musl 1.2.5 is vendored and 1345 of its sources are compiled unmodified. The
redirection is one header: musl reaches its kernel through seven inline
functions declared once per architecture, and port/include/syscall_arch.h
replaces that architecture's copy.

Five of musl's own sources are replaced, each because it reads the shape of one
environment rather than asking a kernel for something: the auxiliary vector,
the program's own ELF headers, the instruction that installs a thread pointer,
the system call that creates a thread, and the duplication of the calling
image. Four names have no C to fall back on, and one assembly file that
assembles for every object format supplies three of them; the fourth needed
assembly only in order to issue a system call from a known instruction, and
there is no such instruction here.

The port layer is 1995 lines. It contains the two structures clause 7.1 forbids
an implementation to have - a table of descriptors and a resolver of names -
and their being above the boundary rather than below it is the arrangement
rather than a violation of it: there is one of each for every environment
instead of one per environment.

Facilities openkal does not have are refused rather than quietly accepted.
mprotect, sigaction with a handler, fork, pipe, poll, readlink, chmod and
getrandom all report ENOSYS, and the README records what each costs, because a
call that reports success and does nothing is the one answer that leaves a
program wrong without telling it.

examples/wordcount is an ordinary POSIX program whose source mentions none of
this and whose three counts are compared against the system's own wc.
examples/posix makes 32 assertions, each observed by its effect.
…r clang as well as gcc

The feature the implementation is asked for is renamed: what it selects is not
'a program with no C library' but 'this implementation is the whole of the
program's environment', which is a statement about the program and is expressed
without reference to what kind of program it is.

The link line moves to a per-platform table and gains two statements it needed.
The C++ runtime is not embedded: this package is C, the implementation beneath
it is C++ only because the specification's declarations are modules, and a
runtime attached by the driver would have its own references to a C library
resolved by this one. --no-dynamic-linker removes the interpreter clang records
even for a static link, which gcc does not, and which the loader refuses to
start.
Three properties of that environment stand between musl and it, and each was
measured rather than assumed.

A long is narrower than a pointer there. musl states its data model in three
lines of one generated header, so a second generated header states the other
one; two declarations that carry a machine word through a long are widened to
the type musl already provides for the purpose, and two sources that do the same
are replaced. musl/PATCHES.md lists all of them.

A wide character is sixteen bits there and thirty-two in musl. Three of musl's
sources write a wide literal; each writes an array instead. The two are still
different, deliberately: a program that writes L"..." is told so by the
compiler, where narrowing musl's wchar_t would have converted every code point
above U+FFFF to the wrong value silently.

A weak symbol is not a definition there --- measured with both toolchains, for
aliases and definitions, functions and data. musl gives almost every public name
to a definition through a weak alias, and provides 46 names as placeholders for
another source to replace. The aliases are made strongly and the placeholders
are not made at all, decided by the name of the target, which needs no change to
musl.

One further finding is not about that environment but about this port. musl's
per-context state was reached through a variable declared thread_local, which
openkal reports as a property a started context has. On one toolchain such a
variable is reached through a helper that allocates, and the allocator is what
this library provides: the dependency is circular and appears only as a program
that starts and never returns. The state is now kept in a table keyed on the
identity openkal already gives every context, so this port depends on nothing
beyond openkal and runs above an implementation whose contexts have no
thread-local storage at all.

The wordcount example above this library on Windows reports the same three
counts as that system's own tool, and the thirty-two assertions of the posix
example all hold there.
…two openkal additions

macOS is the third system this library runs on, and reaching it needed two
things and no more.

A second name for a definition cannot be made there the way it is made on ELF or
on PE: the compiler refuses the construct outright. What that format does have is
the assembler's own directive, and the measurement that establishes it is in
PATCHES.md. The name it makes is strong, so the placeholder list that already
existed for the other format applies unchanged --- which is the return on having
decided which names are placeholders by the name of the target rather than by a
list of sites.

The file that supplies what an object format does not is no longer named for one
format. On the third, the loader has already run every initialiser in the image
by the time control reaches the entry point, so the loops that drive them are
absent there and would otherwise run every constructor twice.

Three operations are expressed differently from what their names say, and each
is recorded in PATCHES.md rather than left to be discovered. execve is starting a
program, waiting for it and ending with its status, because openkal has no
operation that replaces a running image and clause 3.1 declines to simulate what
cannot be supplied. utimensat asks for write access where the interface it
implements asks for ownership. A program named without a suffix is tried with one
of the environments' suffix second, which is what every C library for that
environment does and is here rather than beneath because openkal is deliberately
literal about names.

utimensat is now answered at all, through openkal 0.5's kal_fs_set_modified.
Before this, `touch' reported that the operation was not implemented.

tools/working-trees.sh places the specification and the implementation for the
system beside this package and points every manifest at them, which is what the
continuous integration needs and what a contributor needs for the same reason.
…zero and letting the dereference say it

An enquiry that finds no entry answered zero, which the caller reads as a
pointer to the per-context state. Before the first context registers that is the
ordinary state and zero is right; afterwards it is not, and what a reader is
shown is a null dereference several frames from the absence that caused it.

The debugger is also asked at the right moment: a batch that lists its commands
after `run' gets as far as the stop and quits, which is why the first attempt
printed one frame and no backtrace.
…e embedded into it

The build tool decides whether to embed a C++ runtime, and on one of the three
systems its answer is to embed one and to run that runtime's initialiser before
anything else in the image, including the entry point --- because that format
has no priority-ordered initialiser section and the runtime's streams must be
constructed before any global that touches them.

A program above this library carries no other runtime. At that moment the C
library has not started: it has no per-context state, no auxiliary vector and no
allocator. The embedded initialiser reaches the library anyway, through a
guarded static and a mutex, and what a reader is shown is a null dereference in
the mutex --- six frames from the ordering that caused it.

`cxx_runtime = "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 rather than on this library's.

It is declared by the program rather than here because the build tool reads it
from the program: a dependency that declared it would be declaring it for
itself. It is one line, it is recorded in the README beside the dependency, and
both examples carry it.
musl files <bits/float.h> under arch/<arch>/, and for every system musl was
written for the architecture does decide what a long double is. One of the three
systems this port builds for disagrees: on its aarch64 a long double is a
double, where the architecture's own procedure call standard says quadruple.

    target             __LDBL_MANT_DIG__   arch/<arch>/bits/float.h
    aarch64, Apple             53                   113
    x86_64,  Apple             64                    64
    x86_64,  Linux             64                    64
    aarch64, Linux            113                   113

Every routine that takes a long double then reads sixteen bytes out of eight.
The first one a program reaches is the one that formats a floating-point number:
printf("%.3f", 1.5) stopped in frexpl with the value read as infinity, and what
the report named was the arithmetic rather than the header.

port/include/bits/float.h states the compiler's own answer on that combination,
through the macros the compiler defines, so it cannot drift from what is being
compiled --- which is the property the header it replaces did not have.
port/src/okm_float_assert.c asserts the agreement for every target, so a fourth
combination that disagrees is a failed build naming the field rather than a
program that formats a number wrongly.

musl already supports a 53-bit long double: it is what its own arm and riscv64
configurations use, and every conditional in src/math and src/stdio is written
for it. Nothing beyond the one header is involved.
@Sunrisepeak
Sunrisepeak merged commit 11aa86c into main Aug 20, 2026
4 checks passed
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