Skip to content

Compose sockets, datagrams, readiness and fork above openkal 0.8 - #16

Merged
Sunrisepeak merged 6 commits into
mainfrom
feat/capability-completeness
Aug 27, 2026
Merged

Compose sockets, datagrams, readiness and fork above openkal 0.8#16
Sunrisepeak merged 6 commits into
mainfrom
feat/capability-completeness

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Reported as mcpplibs/openkal-linux#13 and mcpplibs/openkal-musl#13: a
program above this library was told ENOSYS for socket, bind, listen,
accept, connect, poll, select and fork, and could not declare hidden
as an ordinary identifier.

openkal 0.8 added the atoms for all of the first group. Nothing above them used
any of them: musl's network sources compile and issue system calls the
dispatcher had no case for. The gap was here, not in musl's sources and not in
the specification.

What is added

port/src/okm_net.c the socket family upon openkal.net, the datagram family upon openkal.datagram
port/src/okm_poll.c readiness upon openkal.timeout
port/src/okm_fork.c duplicating the calling image upon openkal.space

⚠️ BSD makes a socket first and decides what it is afterwards; openkal has no
object between "nothing" and "a connection".
So a descriptor made by socket
holds the three numbers it was given, and the openkal operation happens at the
call that says what the socket is for — bind records an endpoint, listen
spends it, connect spends its own. A caller cannot observe the deferral: every
error a kernel reports at bind this port reports at listen, one call later
and with the same value.

⚠️ openkal has no operation reporting whether a transfer would proceed, and
clause 6.3 records why. poll therefore attempts the transfer under a bound and
keeps what it produced — one byte, one connection, one message — for the
operation that follows. POLLIN asserts that a read will not block, and a byte
already in hand is the strongest form that assertion can take.

Two corrections to the plan this implements

⚠️ posix_spawn was never missing. The plan said the exclusion of
musl/src/process/posix_spawn.c had to be answered by a port implementation.
port/src/okm_spawn.c has replaced that source since the port was written,
and system and popen work through it. Measured: system("exit 5") returns
an exit status of 5, popen carries a line back. What was missing was a
criterion — examples/subprocess is it.

⚠️⚠️ timeout_ns = 0 does not mean "do not wait". It means no bound;
timeout.h says so and kal_task_wait established it. Passing a caller's zero
straight through turns the one call that must not wait into the one that never
returns — measured, as a hang, four lines into the network probe.

⚠️ And SYS_clone is not the only number fork arrives through. musl's
_Fork issues SYS_fork where the architecture has it, so a dispatcher
implementing only SYS_clone is reached on aarch64 and riscv64 and never on
x86_64.

fork is composed here, and the specification asked for it

space.h states in terms that a library above reaches fork by saving its own
execution state before the call and restoring it in the started context, and
that this belongs above the line. okm_setjmp.S already carried the
per-architecture half.

An earlier reading of the report concluded that fork had been declined
deliberately. Half of that is right: clause 7.1 declines to duplicate an address
space and its execution state. openkal.space supplies the first half by
itself, and what was missing was never an atom.

What is refused, and why each refusal is stated rather than approximated

  • permission bitskal_node_info carries a boolean writable, not a mode
    word. Mapping the owner-write bit onto it would make chmod(0600) succeed and
    stat report something else.
  • symbolic linksSURFACE.txt has no operation that creates one. It does
    have kal_node_link and KAL_FS_PROP_LINKS, so an implementation can report a
    link and cannot make one; the asymmetry is the specification's and is recorded.
  • out-of-band data, readiness sets (epoll stays withheld).
  • O_NONBLOCK where openkal.timeout is absent — it used to be accepted and
    carried no further, which is the one shape this port exists to avoid.

Criteria

Four probes, each written against POSIX, naming no openkal symbol, and each
stating on its command line what it expects the backend beneath to provide — so
a refusal is asserted as a refusal rather than merely not asked about.

examples/net 35 observations over a listener, a connection and two datagram endpoints
examples/subprocess another program started three ways; --no-fork requires the refusal
examples/identifiers hidden, weak, weak_alias as the program's own names — a compile-time criterion
examples/posix unchanged, 32 observations

And in continuous integration: eleven names asserted weak, with
kal_time_sleep as the strong control. A strong reference to any optional
interface would make a bare-metal program that never opens a socket fail to
link — the defect that step already exists because of, arriving through a new
name.

⚠️ The first form of that check reported two failures, and both were the
check's fault
: a search under target/ reaches the dependency's objects,
where openkal-linux's kal_timeout_accept refers to its own kal_net_accept
strongly. That is correct for an implementation and says nothing about this
port.


This is one change across seven repositories, and every branch carries the
same name.
Each repository's continuous integration substitutes its siblings'
working trees taken from the branch of the name under test, so the graph only
holds together when they agree.

repository what it carries
openkal-musl the socket, datagram, readiness and image-copying routes, and four probes
openkal-macos the five interfaces 0.8 added
openkal-windows four of the five; openkal.space declined in terms
openkal-linux the conformance run selects the optional interfaces, which nothing did
openkal-llvm-runtime __config_site's claims asserted by a program, and the C library repinned
openkal the portable example's pins, and §9 of the plan
sbase ninety-seven utilities above the new C library

The specification does not change. No interface was added, none was altered,
and SURFACE.txt is untouched — every capability below is composed from atoms
openkal 0.8 already had.

The specification grew five interfaces in 0.8 and this port used none of
them. A program above this library reached `socket' and was told ENOSYS
because musl's network sources compile and issue system calls the
dispatcher had no case for; `poll' and `select' the same; `fork' the
same. Reported as mcpplibs/openkal-linux#13. The gap was here.

  port/src/okm_net.c   the socket family upon openkal.net, the datagram
                       family upon openkal.datagram. BSD makes a socket
                       first and decides what it is afterwards, and
                       openkal has no object between nothing and a
                       connection --- so a descriptor made by `socket'
                       holds three numbers and the openkal operation
                       happens at the call that says what it is for.
  port/src/okm_poll.c  readiness upon openkal.timeout. openkal has no
                       operation reporting whether a transfer would
                       proceed (clause 6.3 records why), so `poll'
                       attempts the transfer under a bound and KEEPS
                       what it produced --- one byte, one connection,
                       one message --- for the operation that follows.
  port/src/okm_fork.c  duplicating the calling image upon openkal.space.
                       space.h describes this composition and says in
                       terms that it belongs above the line: `setjmp'
                       before the call, `longjmp' in the copy.

An earlier reading of the report concluded that `fork' had been declined
deliberately. Half of that is right: clause 7.1 declines to duplicate an
address space AND ITS EXECUTION STATE. `openkal.space' supplies the
first half by itself, and what was missing was never an atom.

`posix_spawn' was likewise reported as missing and is not: okm_spawn.c
has replaced musl's since this port was written, and `system' and
`popen' work through it. What was missing there was a criterion.

Every reference to the four interfaces is weak and exactly one name per
interface is tested, because clause 3 requires an implementation to
provide an interface in whole or not at all. A strong reference would
turn "this backend has no network" into "no program above this library
links", which this port has already been bitten by twice.

Refused rather than approximated: permission bits, because
`kal_node_info' carries one boolean and not a mode word; symbolic links,
because SURFACE.txt has no operation that makes one; out-of-band data;
readiness sets. `O_NONBLOCK' is now refused where `openkal.timeout' is
absent, where it used to be accepted and carried no further.

Also scopes musl's internal `hidden', `weak' and `weak_alias' to the
overlay that defines them, so a program above this library may use the
names (openkal-musl#13). `restrict' stays unconditional in C++: musl's
PUBLIC headers write it, which is a property of those headers.

Four probes, each written against POSIX and naming no openkal symbol,
and each stating on its command line what it expects the backend beneath
to provide --- so that a refusal is asserted as a refusal rather than
merely not asked about.
The step asserted the rule for one name. There are eleven now: the four
interfaces okm_net.c, okm_poll.c and okm_fork.c reach are optional in
exactly the sense `openkal.random' is, and openkal-opensbi and
openkal-uefi decline all four. A strong reference to any of them would
make a bare-metal program that never opens a socket fail to link, which
is the defect this step already exists because of.

⚠️⚠️ AND THE FIRST FORM OF THE CHECK MEASURED THE WRONG OBJECTS, which is
worth recording because it reported two real-looking failures. A search
of everything under `target/' called `kal_net_accept' and
`kal_datagram_recv_from' undefined STRONG references --- because the
DEPENDENCY's objects are there too, under `obj/mcpplibs_*', and
openkal-linux's `kal_timeout_accept' refers to its own `kal_net_accept'
strongly. That is correct for an implementation and says nothing about
this port.

⇒ `-maxdepth 1', and one fingerprint directory asserted before anything
is read. The step now examines 1341 of this package's own objects and
reports the number, so a search that found nothing cannot pass.
⚠️⚠️ `tools/run-probe.sh' DID NOT PARSE, ON ALL FOUR ROWS AT ONCE.

    name="${2:?the program's name}"

Bash parses the text of `${2:?...}' with quoting active, so the
apostrophe opens a single quote that never closes --- and the report
arrives thirty lines later as

    tools/run-probe.sh: line 53: syntax error near unexpected token `('

naming a line that is correct.

⚠️ NOTHING LOCAL HAD RUN THE SCRIPT. The four probes were exercised by
running their binaries directly, so the runner --- which is what CI
actually invokes --- was written and never executed. `bash -n' reproduces
it in a second, which is the whole cost of the check that was not made.

And `examples/identifiers' now prints the same last line every other
probe prints, so one runner reads all four rather than three plus a
bespoke step. Its criterion is unchanged and is still the compile.
⚠️⚠️ okm_context.c keeps this library's per-context state --- its error
value, its locale, its thread record --- in a table keyed on
`kal_task_current()'. The specification says that identity is "unique
among contexts running at the same moment and may be reused after one
ends". It says NOTHING about a copy of the address space, and the two
implementations answer differently:

    openkal-linux   caches `gettid' in a thread-local, so the COPY of the
                    cache answers the parent's value and the lookup works
    openkal-macos   asks `thread_selfid' every time, so the started
                    context is a NEW thread of a NEW process and answers
                    a value the table has never seen

⇒ The second is not a defect. It is the honest answer to the question the
interface asks, and the assumption that a copy keeps its identity was
this port's.

⭐ MEASURED, AND THE PORT'S OWN DIAGNOSTIC NAMED IT. On the macOS row the
copy stopped with

    openkal-musl: this execution context has no per-context state --- the
    implementation's kal_task_current did not answer the same value here
    as it did when the context started

which is the message `__okm_get_tp' has carried since it was written, for
exactly this condition. Without it the report would have been a copy that
ended on a signal, four layers from the cause --- and the probe reported
only "it did not report the status it was written to report", which names
a fault and not a place. The probe now prints the raw status when it is
wrong, for the same reason.

The started context therefore rebinds its slot before anything reads
per-context state, from two globals written in the original --- a local
written between `setjmp' and `longjmp' is indeterminate in the resumed
context.
Found reviewing the composition rather than by a failure, which is why it
is worth stating: the table of started programs lives in this port's own
memory, so a copy of the address space inherits it --- and POSIX is
explicit that a duplicate has no children.

Left in place the entries are worse than useless. A copy that called
`wait' would be told about a program it did not start and cannot wait
for, and `system' inside a copy --- which waits for the child it just
started --- could be handed one of the original's instead.

The handles are not released: they belong to the original, which is still
holding them. Only the copy's view of them is cleared, in the started
context, before anything reads the table.
@Sunrisepeak
Sunrisepeak merged commit ea7fd08 into main Aug 27, 2026
6 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.

2 participants