From d32840ba8dbb8bd01879f5242d54e9c04d729300 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 21:21:07 +0800 Subject: [PATCH 1/3] Provide the five interfaces openkal 0.8 added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implementation declined net, datagram, timeout, exec and space in whole, which clause 3 permits and clause 6.1 makes honest. The consequence was that every capability composed above them --- sockets, readiness and the duplication of the calling image, all of which openkal-musl now routes --- was a Linux capability rather than an ecosystem one. src/net.cpp the kernel's own socket calls. There is no `accept4' and no flag upon `socket' that closes a descriptor across a spawn, so close-on-exec is set afterwards; process.cpp already records what the two steps cost. src/datagram.cpp the same calls with SOCK_DGRAM. src/timeout.cpp `poll', whose bound is stated in milliseconds. `ppoll' does not exist here, so a millisecond is the granularity this implementation reports. src/space.cpp the duplication primitive openkal.process was already built on. ⚠️ The duplicate is distinguished by the call's SECOND return value: both images receive a process identifier in the first. src/exec.cpp a mapping obtained writable and made executable afterwards, which is the only order this system permits. ⚠️ TWO CONSTANTS DIFFER FROM THE OTHER KERNEL'S AND WOULD NOT ANNOUNCE IT: AF_INET6 is 30 here and 10 there, SOL_SOCKET is 0xffff here and 1 there. A third difference is structural --- a socket address carries its own length in its first byte, where the other kernel's layout puts the low half of a two-byte family. ⚠️ Clause 6.5 names this system and openkal.exec in terms. That clause is about memory writable and executable AT THE SAME TIME, which this interface does not offer: a region is writable, then published, then executable, and never both. The conformance suite reserves a region, writes instructions into it, publishes it, CALLS it and compares what it returned, so the reading is settled by the system rather than by a comment. If it stops holding, the remedy is clause 6.5's: a feature provided at dependency resolution. The conformance run moves from `full' to `full,optional', which is what makes the five above examined rather than skipped, and numbers.yml gains the socket calls so that their numbers keep being read from the system's own table rather than remembered. --- .github/workflows/ci.yml | 2 +- .github/workflows/numbers.yml | 21 +++- README.md | 30 ++++- mcpp.toml | 2 +- src/datagram.cpp | 153 +++++++++++++++++++++++++ src/endpoint.h | 120 ++++++++++++++++++++ src/exec.cpp | 97 ++++++++++++++++ src/net.cpp | 202 ++++++++++++++++++++++++++++++++++ src/space.cpp | 68 ++++++++++++ src/sys.h | 62 ++++++++++- src/timeout.cpp | 165 +++++++++++++++++++++++++++ 11 files changed, 914 insertions(+), 8 deletions(-) create mode 100644 src/datagram.cpp create mode 100644 src/endpoint.h create mode 100644 src/exec.cpp create mode 100644 src/net.cpp create mode 100644 src/space.cpp create mode 100644 src/timeout.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32a25a1..e6206d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,7 +180,7 @@ jobs: - name: Every interface, every kind of examination run: | - bash .spec/tools/run-conformance.sh openkal-macos . full + bash .spec/tools/run-conformance.sh openkal-macos . full,optional # The other architecture, as far as this system allows it to be reached. # diff --git a/.github/workflows/numbers.yml b/.github/workflows/numbers.yml index 829296c..9bbb462 100644 --- a/.github/workflows/numbers.yml +++ b/.github/workflows/numbers.yml @@ -49,7 +49,11 @@ jobs: ulock_wait ulock_wake __ulock_wait __ulock_wake \ futimens utimensat futimes utimes settimeofday \ fchmod fchmodat fcntl \ - pipe sigaction; do + pipe sigaction \ + socket connect accept accept_nocancel bind listen shutdown \ + getsockname getpeername setsockopt getsockopt \ + sendto recvfrom sendmsg recvmsg socketpair \ + poll select pselect kevent; do printf '%-24s' "$n" grep -E "^#define[[:space:]]+SYS_${n}[[:space:]]" "$h" | head -1 || echo '(absent)' done @@ -138,6 +142,21 @@ jobs: { "mkdirat", 475, -2, 0, 0, 0 }, { "ulock_wait", 515, 0, 0, 0, 0 }, { "ulock_wake", 516, 0, 0, 0, 0 }, + /* The socket calls, with a descriptor that cannot be one. */ + { "recvfrom", 29, -1, 0, 0, 0 }, + { "accept", 30, -1, 0, 0, 0 }, + { "getpeername", 31, -1, 0, 0, 0 }, + { "getsockname", 32, -1, 0, 0, 0 }, + { "select", 93, 0, 0, 0, 0 }, + { "socket", 97, 0, 0, 0, 0 }, + { "connect", 98, -1, 0, 0, 0 }, + { "bind", 104, -1, 0, 0, 0 }, + { "setsockopt", 105, -1, 0, 0, 0 }, + { "listen", 106, -1, 0, 0, 0 }, + { "getsockopt", 118, -1, 0, 0, 0 }, + { "sendto", 133, -1, 0, 0, 0 }, + { "shutdown", 134, -1, 0, 0, 0 }, + { "poll", 230, 0, 0, 0, 0 }, }; int main(void) { diff --git a/README.md b/README.md index dbb5e96..0ad3242 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ written on the kernel's own calls. ```toml [dependencies] -openkal = "0.5.1" +openkal = "0.8.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.3.1" +openkal-macos = "0.5.0" ``` Its purpose is as much to test the specification as to be used. A specification @@ -18,9 +18,31 @@ different system is what turns the claim into an observation. ## Interfaces provided -All eight. `tools/check-surface.sh --complete` in the specification package +All fifteen. `tools/check-surface.sh --complete` in the specification package compares the exported names against `SURFACE.txt`. +The five that openkal 0.8 added were declined by this implementation until now, +and each is provided in whole: + +| | on this system | +| --- | --- | +| `openkal.net` | the kernel's own socket calls. There is no `accept4` and no flag upon `socket` that closes a descriptor across a spawn, so close-on-exec is set afterwards with `fcntl` | +| `openkal.datagram` | the same calls, with `SOCK_DGRAM` | +| `openkal.timeout` | `poll`, whose bound is stated in milliseconds. `ppoll` does not exist here, so a millisecond is the granularity this implementation reports — what the environment can distinguish rather than what would be convenient | +| `openkal.space` | the duplication primitive `openkal.process` was already built on. ⚠️ The duplicate is distinguished by the call's **second** return value: both images receive a process identifier in the first, so an implementation testing that one against zero would decide that neither image was the duplicate | +| `openkal.exec` | a mapping obtained writable and made executable afterwards, which is the only order this system permits | + +**⚠️ Clause 6.5 names this system and `openkal.exec` in terms**, and says +availability may be settled by how the artifact is *produced* — a signed +declaration applied after the link. That clause is about memory which is +writable and executable **at the same time**, which is not what this interface +offers: a region here is writable, then published, then executable, and never +both. The conformance suite reserves a region, writes an instruction sequence +into it, publishes it, **calls it**, and compares what it returned — so the +reading is settled by the system rather than by this paragraph. If that +observation stops holding, the remedy is clause 6.5's: a feature of this +package, provided at dependency resolution. + The package exports no module: the interface belongs to the specification, and this package supplies definitions. @@ -118,7 +140,7 @@ rather than the specification. ```bash git clone https://github.com/mcpplibs/openkal .spec -bash .spec/tools/run-conformance.sh openkal-macos . full +bash .spec/tools/run-conformance.sh openkal-macos . full,optional ``` ## Architectures diff --git a/mcpp.toml b/mcpp.toml index 38d705a..fc8a9c1 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.4.0" +version = "0.5.0" description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used." license = "Apache-2.0" diff --git a/src/datagram.cpp b/src/datagram.cpp new file mode 100644 index 0000000..1074ebc --- /dev/null +++ b/src/datagram.cpp @@ -0,0 +1,153 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.datagram upon this kernel's socket calls. +// +// A DATAGRAM IS NOT PACKED AS A kal_stream, and the handle type is its own for +// that reason: kal_stream_read reports a count and not a boundary, so reading a +// datagram through it would lose the property that distinguishes this interface. +// The packing is the same, the type is not, and the type is what prevents the +// mistake. + +namespace { + +int fd_of(kal_datagram d) { return okm::unpack(d.h); } + +} // namespace + +extern "C" { + +int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) { + if (out == nullptr) return kal_err_invalid; + + // A null local endpoint asks for one that may send and whose receiving + // address is unspecified. IPv4 is chosen for it, because a family must be + // named at the point the socket is made and this is the one every + // environment that has a network at all provides. + okm_long family = okm::af_inet; + if (local != nullptr) { + family = okm::family_of(*local); + if (family < 0) return kal_err_invalid; + } + + const okm_long fd = okm::sys(okm::nr_socket, family, okm::sock_dgram, + okm::ipproto_udp); + if (okm::failed(fd)) return okm::translate(fd); + okm::sys(okm::nr_fcntl, fd, okm::f_setfd, okm::fd_cloexec); + + if (local != nullptr) { + okm::ksockaddr_storage ss{}; + okm_u32 len = 0; + if (const int rc = okm::to_kernel(*local, ss, len); rc != kal_ok) { + okm::sys(okm::nr_close, fd); + return rc; + } + if (const okm_long r = okm::sys(okm::nr_bind, fd, + reinterpret_cast(&ss), + static_cast(len)); + okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + } + + out->h = okm::pack(static_cast(fd)); + if (out->h == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_datagram_local(kal_datagram d, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = fd_of(d); + if (fd < 0) return kal_err_invalid; + + okm::ksockaddr_storage ss{}; + okm_u32 len = static_cast(sizeof ss); + const okm_long r = okm::sys(okm::nr_getsockname, fd, + reinterpret_cast(&ss), + reinterpret_cast(&len)); + if (okm::failed(r)) return okm::translate(r); + return okm::from_kernel(ss, *out); +} + +kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, + const kal_endpoint* to) { + const int fd = fd_of(d); + if (fd < 0 || to == nullptr) return { 0, kal_err_invalid }; + + okm::ksockaddr_storage ss{}; + okm_u32 addrlen = 0; + if (const int rc = okm::to_kernel(*to, ss, addrlen); rc != kal_ok) + return { 0, rc }; + + for (;;) { + const okm_long r = okm::sys(okm::nr_sendto, fd, + reinterpret_cast(buf), + static_cast(len), 0, + reinterpret_cast(&ss), + static_cast(addrlen)); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return { 0, okm::translate(r) }; + + // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface + // states. The kernel reports a count anyway; a count short of the length + // would mean the medium had split the message, which for a datagram + // socket it does not do. Reporting the short count as success would give + // a caller a partial send this interface says cannot occur, so it is + // reported as a failure of the medium instead. + const kal_uintptr n = static_cast(r); + return { n, n == len ? kal_ok : kal_err_io }; + } +} + +kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from) { + const int fd = fd_of(d); + if (fd < 0) return { 0, kal_err_invalid }; + + okm::ksockaddr_storage ss{}; + okm_u32 addrlen = static_cast(sizeof ss); + + for (;;) { + const okm_long r = okm::sys(okm::nr_recvfrom, fd, + reinterpret_cast(buf), + static_cast(len), 0, + reinterpret_cast(&ss), + reinterpret_cast(&addrlen)); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return { 0, okm::translate(r) }; + + // THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was + // sent. Without MSG_TRUNC the kernel already reports the former, which + // is what this interface requires: a caller that trusted the larger + // number would read beyond its own buffer. + if (from != nullptr) { + // A sender whose family this implementation does not know leaves the + // endpoint zeroed rather than partly filled. The transfer still + // happened and is reported; what is unknown is who sent it. + if (okm::from_kernel(ss, *from) != kal_ok) { + for (auto& b : from->addr) b = 0; + from->addr_len = 0; + from->port = 0; + } + } + return { static_cast(r), kal_ok }; + } +} + +void kal_datagram_close(kal_datagram d) { + const int fd = fd_of(d); + if (fd < 0) return; + okm::sys(okm::nr_close, fd); + okm::retire(d.h); +} + +// Broadcast is not claimed. The kernel provides it only after SO_BROADCAST has +// been set, and this interface has no operation that would set it; a word +// claiming a facility no operation reaches is the disagreement clause 6.2 exists +// to prevent. +const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6; + +} // extern "C" diff --git a/src/endpoint.h b/src/endpoint.h new file mode 100644 index 0000000..b7ca840 --- /dev/null +++ b/src/endpoint.h @@ -0,0 +1,120 @@ +// Conversion between kal_endpoint and this kernel's socket address structures. +// +// SHARED BY openkal.net AND openkal.datagram BECAUSE THE TYPE IS. Either +// interface may be provided without the other, so the conversion belongs to +// neither; writing it twice would be one decision stated in two places, and the +// two would eventually disagree about which lengths are accepted. +#pragma once +#include "sys.h" +#include + +namespace okm { + +// The port is carried in host order by kal_endpoint and in network order by the +// kernel. The conversion is written out rather than taken from a C library's +// htons, for the reason the head of sys.h gives. +inline unsigned short to_net_port(kal_u32 port) { + const unsigned short p = static_cast(port & 0xffffu); + return static_cast((p << 8) | (p >> 8)); +} +inline kal_u32 from_net_port(unsigned short net) { + return static_cast((net << 8) | (net >> 8)) & 0xffffu; +} + +// Fills a kernel address from an endpoint, and reports its length. +// +// ⚠️ THE LENGTH IS WRITTEN INTO THE STRUCTURE AS WELL AS RETURNED, because this +// kernel's layout carries one and the other kernel's does not. A structure left +// with a zero there is accepted by some calls and not by others, which is the +// worst of the three possible behaviours. +// +// A LENGTH THIS IMPLEMENTATION DOES NOT KNOW IS REFUSED RATHER THAN READ AS ONE +// IT DOES. The specification defines the set of lengths and allows it to grow; +// an implementation that ignored the field would misread every address a later +// revision defines, and would do so silently. +inline int to_kernel(const kal_endpoint& ep, ksockaddr_storage& out, okm_u32& len) { + fill(&out, 0, sizeof out); + + if (ep.addr_len == 4) { + auto* v4 = reinterpret_cast(&out); + v4->len = sizeof(ksockaddr_in); + v4->family = static_cast(af_inet); + v4->port = to_net_port(ep.port); + okm_u32 a = 0; + for (int i = 0; i < 4; ++i) + a |= static_cast(ep.addr[i]) << (i * 8); // already network order + v4->addr = a; + len = static_cast(sizeof(ksockaddr_in)); + return kal_ok; + } + + // Sixteen bytes is an address; twenty is an address followed by a scope + // identifier, which is carried in the four bytes after it. + if (ep.addr_len == 16 || ep.addr_len == 20) { + auto* v6 = reinterpret_cast(&out); + v6->len = sizeof(ksockaddr_in6); + v6->family = static_cast(af_inet6); + v6->port = to_net_port(ep.port); + v6->flowinfo = 0; + for (int i = 0; i < 16; ++i) v6->addr[i] = ep.addr[i]; + okm_u32 scope = 0; + if (ep.addr_len == 20) + for (int i = 0; i < 4; ++i) + scope |= static_cast(ep.addr[16 + i]) << (i * 8); + v6->scope_id = scope; + len = static_cast(sizeof(ksockaddr_in6)); + return kal_ok; + } + + return kal_err_invalid; +} + +// Fills an endpoint from a kernel address. A family this implementation does +// not know leaves the endpoint zeroed and reports it, for the same reason. +inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) { + for (auto& b : out.addr) b = 0; + out.addr_len = 0; + out.port = 0; + + const unsigned char family = in.pad[1]; // the second byte, per the layout + + if (family == af_inet) { + const auto* v4 = reinterpret_cast(&in); + const okm_u32 a = v4->addr; + for (int i = 0; i < 4; ++i) + out.addr[i] = static_cast((a >> (i * 8)) & 0xffu); + out.addr_len = 4; + out.port = from_net_port(v4->port); + return kal_ok; + } + + if (family == af_inet6) { + const auto* v6 = reinterpret_cast(&in); + for (int i = 0; i < 16; ++i) out.addr[i] = v6->addr[i]; + // A zero scope identifier is reported as the shorter form. The two + // lengths denote the same address when the scope is zero, and reporting + // the shorter one keeps an address that came in as sixteen bytes going + // back out as sixteen. + if (v6->scope_id == 0) { + out.addr_len = 16; + } else { + for (int i = 0; i < 4; ++i) + out.addr[16 + i] = static_cast((v6->scope_id >> (i * 8)) & 0xffu); + out.addr_len = 20; + } + out.port = from_net_port(v6->port); + return kal_ok; + } + + return kal_err_invalid; +} + +// Which socket family an endpoint asks for, or -1 for a length that is not one +// of the defined ones. +inline okm_long family_of(const kal_endpoint& ep) { + if (ep.addr_len == 4) return af_inet; + if (ep.addr_len == 16 || ep.addr_len == 20) return af_inet6; + return -1; +} + +} // namespace okm diff --git a/src/exec.cpp b/src/exec.cpp new file mode 100644 index 0000000..016c3bf --- /dev/null +++ b/src/exec.cpp @@ -0,0 +1,97 @@ +#include "sys.h" +#include + +// openkal.exec on this system. +// +// A mapping obtained writable and made executable afterwards, which is the +// order the interface requires and the only order this system permits: a +// mapping that is writable and executable at once is refused here, and the +// interface states the narrower contract for exactly that reason. +// +// ⚠️⚠️ CLAUSE 6.5 NAMES THIS SYSTEM AND THIS INTERFACE IN TERMS, and what it +// says is that availability may be settled by how the artifact is PRODUCED --- +// a signed declaration applied after the link, by whoever produces the +// artifact. That would make the interface a feature of this package rather than +// a part of it. +// +// ⭐ THIS IMPLEMENTATION PROVIDES IT UNCONDITIONALLY, AND THE READING IS A +// MEASUREMENT RATHER THAN A CHOICE. The declaration clause 6.5 describes is +// what a program needs to obtain memory that is writable and executable AT THE +// SAME TIME --- `MAP_JIT' and the pair of calls that flip one region between +// the two. It is not what a program needs to obtain memory that is writable, +// then made executable and no longer writable, which is the whole of what this +// interface offers. tests/conformance_exec.cpp is where that distinction stops +// being an argument: it reserves a region, writes an instruction sequence into +// it, publishes it, CALLS it, and compares what it returned. +// +// ⇒ If that test fails on this system, the reading above is wrong and the +// remedy is clause 6.5's: a feature of this package, provided at dependency +// resolution, together with whatever produces the artifact. The test is here so +// that the question is answered by the system rather than by this comment. +// +// ⚠️ THE INSTRUCTION CACHE IS INVALIDATED HERE AS WELL, AND THAT DOES NOT MOVE +// THE OBLIGATION. A processor with separate caches for data and instructions +// has just had bytes written through the data path that it is about to fetch +// through the instruction path, and nothing in `mprotect' makes the second path +// see the first's writes. The specification places the maintenance upon the +// PROGRAM --- the conformance suite performs it itself and says why: the +// program is the party that knows which bytes it wrote --- and this +// implementation performs it too, over the region whose protection it just +// changed. A program that also does it pays for one redundant sequence; a +// program that does not is not silently wrong on one of this system's two +// architectures. On x86_64 the builtin expands to nothing. + +namespace { + +constexpr okm_uptr kPage = 4096; + +// ⚠️ THE PAGE IS 16384 BYTES ON ONE OF THIS SYSTEM'S TWO ARCHITECTURES. Rounding +// to the smaller number is still correct --- the kernel rounds up to its own +// granularity, and a region reserved as 4096 occupies a whole page of whatever +// size --- but a caller freeing with the size it reserved must reach the same +// number, which it does because both go through the same rounding. +okm_uptr round_up(okm_uptr n, okm_uptr to) { return (n + to - 1) & ~(to - 1); } + +} // namespace + +extern "C" { + +void* kal_exec_alloc(kal_uintptr size) { + if (size == 0) return nullptr; + const okm_uptr bytes = round_up(static_cast(size), kPage); + const okm_long r = okm::sys(okm::nr_mmap, 0, static_cast(bytes), + okm::prot_read | okm::prot_write, + okm::map_private | okm::map_anon, -1, 0); + if (okm::failed(r)) return nullptr; + return reinterpret_cast(r); +} + +int kal_exec_publish(void* p, kal_uintptr size) { + if (p == nullptr || size == 0) return kal_err_invalid; + const okm_uptr bytes = round_up(static_cast(size), kPage); + const okm_long r = okm::sys(okm::nr_mprotect, reinterpret_cast(p), + static_cast(bytes), + okm::prot_read | okm::prot_exec); + if (okm::failed(r)) return okm::translate(r); + + auto* begin = static_cast(p); + __builtin___clear_cache(begin, begin + bytes); + return kal_ok; +} + +void kal_exec_free(void* p, kal_uintptr size) { + if (p == nullptr || size == 0) return; + const okm_uptr bytes = round_up(static_cast(size), kPage); + okm::sys(okm::nr_munmap, reinterpret_cast(p), + static_cast(bytes)); +} + +// A published region may NOT be reserved for writing again on this system, and +// the position is withheld accordingly. Asking this kernel to make an executable +// mapping writable is the case it refuses, which is the whole reason the +// interface separates the two states; a caller that must change published bytes +// reserves a second region and abandons the first, which is what the header +// says a zero here means. +const kal_uintptr kal_exec_props = 0; + +} // extern "C" diff --git a/src/net.cpp b/src/net.cpp new file mode 100644 index 0000000..c1ed06f --- /dev/null +++ b/src/net.cpp @@ -0,0 +1,202 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.net upon this kernel's socket calls. +// +// A CONNECTION IS AN OWNED HANDLE AND THE STREAM IS BORROWED FROM IT, exactly as +// kal_file and kal_fs_stream are here. The owned handle carries a generation so +// that a released one stops being valid, which clause 7.2 requires; the stream +// it yields is the bare descriptor, because that is what openkal.stream's +// transfer operations take. +// +// ⚠️ TWO DIFFERENCES FROM THE OTHER KERNEL, BOTH IN THE SHAPE OF THE CALLS +// RATHER THAN IN WHAT THEY DO. +// +// There is no `accept4' and no flag upon `socket' that closes a descriptor +// across a spawn, so close-on-exec is set afterwards with `fcntl'. The two +// steps are not equivalent to one under a concurrent spawn, and process.cpp +// already records that this implementation states the difference rather than +// concealing it. +// +// A socket address carries its own length in its first byte. endpoint.h +// writes it; a structure copied from the other kernel would put the family +// where this one reads a length. + +namespace { + +int fd_of(kal_net_conn c) { return okm::unpack(c.h); } +int fd_of(kal_net_listener l) { return okm::unpack(l.h); } + +void close_on_exec(okm_long fd) { + okm::sys(okm::nr_fcntl, fd, okm::f_setfd, okm::fd_cloexec); +} + +int report_address(okm_long call, int fd, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + if (fd < 0) return kal_err_invalid; + okm::ksockaddr_storage ss{}; + okm_u32 len = static_cast(sizeof ss); + const okm_long r = okm::sys(call, fd, reinterpret_cast(&ss), + reinterpret_cast(&len)); + if (okm::failed(r)) return okm::translate(r); + return okm::from_kernel(ss, *out); +} + +} // namespace + +extern "C" { + +int kal_net_connect(const kal_endpoint* to, kal_net_conn* out) { + if (to == nullptr || out == nullptr) return kal_err_invalid; + const okm_long family = okm::family_of(*to); + if (family < 0) return kal_err_invalid; + + okm::ksockaddr_storage ss{}; + okm_u32 len = 0; + if (const int rc = okm::to_kernel(*to, ss, len); rc != kal_ok) return rc; + + const okm_long fd = okm::sys(okm::nr_socket, family, okm::sock_stream, + okm::ipproto_tcp); + if (okm::failed(fd)) return okm::translate(fd); + close_on_exec(fd); + + for (;;) { + const okm_long r = okm::sys(okm::nr_connect, fd, + reinterpret_cast(&ss), + static_cast(len)); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + break; + } + + out->h = okm::pack(static_cast(fd)); + if (out->h == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { + if (local == nullptr || out == nullptr) return kal_err_invalid; + const okm_long family = okm::family_of(*local); + if (family < 0) return kal_err_invalid; + + okm::ksockaddr_storage ss{}; + okm_u32 len = 0; + if (const int rc = okm::to_kernel(*local, ss, len); rc != kal_ok) return rc; + + const okm_long fd = okm::sys(okm::nr_socket, family, okm::sock_stream, + okm::ipproto_tcp); + if (okm::failed(fd)) return okm::translate(fd); + close_on_exec(fd); + + // SO_REUSEADDR, because a listener whose predecessor is in the kernel's + // lingering state would otherwise be refused for a reason that has nothing + // to do with the caller. A program restarted within the linger interval is + // the ordinary case, not an unusual one. + { + const int on = 1; + okm::sys(okm::nr_setsockopt, fd, okm::sol_socket, okm::so_reuseaddr, + reinterpret_cast(&on), + static_cast(sizeof on)); + } + + if (const okm_long r = okm::sys(okm::nr_bind, fd, + reinterpret_cast(&ss), + static_cast(len)); + okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + + // The backlog the kernel is asked for. A number rather than a name, because + // this interface does not expose one and a caller has no way to state it. + if (const okm_long r = okm::sys(okm::nr_listen, fd, 128); okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + + out->h = okm::pack(static_cast(fd)); + if (out->h == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_accept(kal_net_listener l, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = fd_of(l); + if (fd < 0) return kal_err_invalid; + + for (;;) { + const okm_long r = okm::sys(okm::nr_accept, fd, 0, 0); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return okm::translate(r); + close_on_exec(r); + out->h = okm::pack(static_cast(r)); + if (out->h == 0) { okm::sys(okm::nr_close, r); return kal_err_no_memory; } + return kal_ok; + } +} + +kal_uintptr kal_net_stream(kal_net_conn c) { + // The bare descriptor, for the reason kal_fs_stream gives: openkal.stream's + // operations take whatever the environment's transfer calls take, and a + // packed word is not that. + const int fd = fd_of(c); + return fd < 0 ? 0u : static_cast(fd); +} + +int kal_net_peer(kal_net_conn c, kal_endpoint* out) { + return report_address(okm::nr_getpeername, fd_of(c), out); +} + +int kal_net_local(kal_net_conn c, kal_endpoint* out) { + return report_address(okm::nr_getsockname, fd_of(c), out); +} + +int kal_net_listener_local(kal_net_listener l, kal_endpoint* out) { + return report_address(okm::nr_getsockname, fd_of(l), out); +} + +int kal_net_shutdown(kal_net_conn c, int direction) { + const int fd = fd_of(c); + if (fd < 0) return kal_err_invalid; + + // The kernel numbers the directions from zero and this interface from one, + // so the mapping is written out rather than arithmetic upon the argument. A + // direction this interface does not define is refused rather than passed + // through, because the kernel would read an unknown number as SHUT_RD. + okm_long how; + switch (direction) { + case KAL_SHUT_READ: how = 0; break; + case KAL_SHUT_WRITE: how = 1; break; + case KAL_SHUT_BOTH: how = 2; break; + default: return kal_err_invalid; + } + + const okm_long r = okm::sys(okm::nr_shutdown, fd, how); + if (okm::failed(r)) return okm::translate(r); + return kal_ok; +} + +void kal_net_close(kal_net_conn c) { + const int fd = fd_of(c); + if (fd < 0) return; + okm::sys(okm::nr_close, fd); + okm::retire(c.h); +} + +void kal_net_close_listener(kal_net_listener l) { + const int fd = fd_of(l); + if (fd < 0) return; + okm::sys(okm::nr_close, fd); + okm::retire(l.h); +} + +// Both positions hold on this kernel: it speaks IPv6 and its `shutdown' ends +// transfer in one direction while the other continues. +const kal_uintptr kal_net_props = KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; + +} // extern "C" diff --git a/src/space.cpp b/src/space.cpp new file mode 100644 index 0000000..1204033 --- /dev/null +++ b/src/space.cpp @@ -0,0 +1,68 @@ +#include "sys.h" +#include + +// openkal.space upon this kernel's duplication primitive. +// +// THE PRIMITIVE IS ALREADY IN src/sys.h AND WAS ALREADY IN USE: openkal.process +// starts a program by duplicating this one and replacing the duplicate, so the +// call, its two return values and the reason the second one matters were +// written down before this interface existed. What is added here is the other +// use of the same primitive --- the one where the duplicate is not replaced. +// +// ⚠️ THE DUPLICATE IS DISTINGUISHED BY THE SECOND VALUE THE CALL RETURNS AND +// NOT BY THE FIRST. Both images receive a process identifier in the first +// register on this system: the original receives the duplicate's and the +// duplicate receives the ORIGINAL's, so an implementation that tested the first +// against zero --- which is what the other kernel's convention would suggest --- +// would decide that neither image was the duplicate. src/sys.h records the +// measurement beside the call. + +extern "C" { + +int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, + kal_process* out) { + if (entry == nullptr || out == nullptr) return kal_err_invalid; + + // THE STACK ARGUMENT IS ACCEPTED AND IGNORED, AND THE HEADER STATES THAT AS + // ONE OF THE TWO PERMITTED BEHAVIOURS. This kernel's primitive gives the + // started context a copy of the caller's own stack, so there is nothing for + // a caller-supplied one to be used for; a caller cannot observe which of the + // two occurred and has no decision resting upon it. + (void)stack_top; + + bool is_duplicate = false; + const okm_long child = okm::duplicate(is_duplicate); + if (okm::failed(child)) return okm::translate(child); + + if (is_duplicate) { + entry(arg); + // THE ENTRY IS NOT REQUIRED TO RETURN, AND IF IT DOES THE CONTEXT ENDS. + // + // Returning from here would return into the duplication's caller in the + // copied space, which is the whole program running a second time from + // the middle of this function. Ending the context is the only defined + // thing to do, and the status says the entry returned rather than + // choosing one. + for (;;) okm::sys(okm::nr_exit, 0); + } + + *out = kal_process{ static_cast(child) }; + return kal_ok; +} + +// Both positions hold on this kernel. +// +// The handles accompany the memory: the duplicate receives a copy of the +// descriptor table, so every handle the caller holds is open in the copy at the +// same number, and the packing in handle.h recovers the same descriptor from the +// same word. +// +// The copy is deferred: this kernel maps the pages copy-on-write, so a store to +// copied memory can fail with the machine out of memory after this call has +// already reported success. An implementation cannot undefer that, and stating +// it is what lets a program that cannot tolerate it know which environment it +// is in. +const kal_uintptr kal_space_props = + KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; + +} // extern "C" diff --git a/src/sys.h b/src/sys.h index ffee25f..e248d4a 100644 --- a/src/sys.h +++ b/src/sys.h @@ -181,8 +181,68 @@ enum : okm_long { nr_fstatat64 = 470, nr_unlinkat = 472, nr_readlinkat = 473, nr_mkdirat = 475, nr_ulock_wait = 515, nr_ulock_wake = 516, + + // The socket calls, and the one that reports readiness. + // + // ⚠️ THEY ARE THE ORIGINAL BSD NUMBERS AND THEY ARE LOW, which is worth + // stating because the numbering here is not the other kernel's in any + // respect: `accept' is 30 there and 288 here, and a table copied from the + // wrong system produces a signal named "bad system call" rather than a + // failed call. .github/workflows/numbers.yml reads them from the system's + // own header on both architectures. + // + // ⭐ THERE IS NO `accept4' AND NO `pipe2' ON THIS SYSTEM, so close-on-exec + // is set after the fact with `fcntl'. process.cpp already records what that + // costs and why a program using these operations from one context is not + // affected by it. + nr_recvfrom = 29, nr_accept = 30, nr_getpeername = 31, nr_getsockname = 32, + nr_select = 93, nr_socket = 97, nr_connect = 98, + nr_bind = 104, nr_setsockopt = 105, nr_listen = 106, + nr_getsockopt = 118, nr_sendto = 133, nr_shutdown = 134, + nr_poll = 230, +}; + +// --- the network's own constants --------------------------------------------- +// +// ⚠️ TWO OF THESE DIFFER FROM THE OTHER KERNEL'S AND WOULD NOT ANNOUNCE IT. +// `AF_INET6' is 30 here and 10 there, and `SOL_SOCKET' is 0xffff here and 1 +// there. A value taken from the wrong system produces a call that fails with +// an ordinary error, which reads as a defect in the caller. +enum : okm_long { + af_inet = 2, af_inet6 = 30, + sock_stream = 1, sock_dgram = 2, + ipproto_tcp = 6, ipproto_udp = 17, + sol_socket = 0xffff, so_reuseaddr = 0x0004, + f_setfd = 2, fd_cloexec = 1, + poll_in = 0x0001, poll_out = 0x0004, + wnohang = 1, }; +// This kernel's socket addresses. ⚠️ THE FIRST BYTE IS A LENGTH, which the +// other kernel's layout does not have: there the family occupies two bytes and +// here it occupies the second byte alone. A structure copied from that system +// puts the family where this one reads a length. +struct ksockaddr_in { + unsigned char len; + unsigned char family; + unsigned short port; // network order + okm_u32 addr; // network order + unsigned char zero[8]; +}; + +struct ksockaddr_in6 { + unsigned char len; + unsigned char family; + unsigned short port; // network order + okm_u32 flowinfo; + unsigned char addr[16]; + okm_u32 scope_id; +}; + +struct ksockaddr_storage { unsigned char pad[128]; }; + +struct kpollfd { int fd; short events; short revents; }; + // --- error values, as this kernel returns them ------------------------------- enum : int { e_perm = 1, e_noent = 2, e_intr = 4, e_io = 5, e_badf = 9, e_child = 10, @@ -203,7 +263,7 @@ enum : okm_long { o_creat = 0x0200, o_excl = 0x0800, o_trunc = 0x0400, o_append = 0x0008, o_directory = 0x100000, o_cloexec = 0x1000000, o_nofollow = 0x0100, at_fdcwd = -2, at_removedir = 0x0080, at_symlink_nofollow = 0x0020, - prot_read = 1, prot_write = 2, + prot_read = 1, prot_write = 2, prot_exec = 4, map_private = 2, map_anon = 0x1000, }; diff --git a/src/timeout.cpp b/src/timeout.cpp new file mode 100644 index 0000000..fec8b7b --- /dev/null +++ b/src/timeout.cpp @@ -0,0 +1,165 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include +#include + +// openkal.timeout upon poll(2) and wait4(2). +// +// THE BOUND IS APPLIED BEFORE THE OPERATION AND NOT DURING IT. `poll' reports +// whether a descriptor would transfer without blocking, so a bounded read is a +// bounded wait for readiness followed by the ordinary read. This is what the +// environment already does at the point of the call, which is why clause 6.3 +// records readiness notification as the alternative that was NOT adopted: an +// interface reporting readiness would oblige an implementation to maintain a set +// and a context of its own, and this one obliges it to maintain nothing. +// +// The bound is therefore upon the WAIT and not upon the transfer. A read that +// becomes ready within the bound and then transfers slowly is not interrupted, +// which is the behaviour every environment's own bounded read has. +// +// ⚠️ THIS KERNEL'S `poll' STATES ITS BOUND IN MILLISECONDS, and `ppoll' --- the +// call the other kernel uses, which takes nanoseconds --- does not exist here. +// The granularity this interface reports is therefore a millisecond, which is +// what the environment can distinguish rather than what would be convenient. + +namespace { + +// A duration of zero denotes no bound, which is the convention kal_task_wait +// establishes. `poll' expresses that with a negative number. +// +// ⚠️ A BOUND SHORTER THAN A MILLISECOND ROUNDS UP TO ONE AND NOT DOWN TO NONE. +// Rounding down would turn a wait into a poll, and the header is explicit: a +// caller that asks for less is not refused and does not get less. +int bound_ms(kal_u64 ns) { + if (ns == 0) return -1; + okm_u64 ms = ns / 1000000ull; + if (ms == 0) ms = 1; + if (ms > 0x7fffffffull) ms = 0x7fffffffull; + return static_cast(ms); +} + +// Waits for one descriptor. Reports kal_ok when it is ready, kal_err_again when +// the bound expired, and a translated error otherwise. +int await(int fd, short events, kal_u64 ns) { + okm::kpollfd p{ fd, events, 0 }; + const int ms = bound_ms(ns); + + const okm_long r = okm::sys(okm::nr_poll, reinterpret_cast(&p), 1, ms); + // AN INTERRUPTED WAIT IS NOT RETRIED WITH THE WHOLE BOUND AGAIN. + // + // Retrying with the original duration would make the bound restart at every + // signal, so a program on a system that delivers them regularly would wait + // without end while appearing to be bounded. The call leaves nothing behind + // to resume from, and the honest report is that the operation did not + // complete. + if (okm::interrupted(r)) return kal_err_again; + if (okm::failed(r)) return okm::translate(r); + if (r == 0) return kal_err_again; // the bound expired + return kal_ok; +} + +} // namespace + +extern "C" { + +kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { + // A transfer of zero bytes does not wait and is not bounded. Waiting first + // would turn a call that always succeeds into one that can expire. + if (len == 0) return { 0, kal_ok }; + + const int fd = okm::unpack(s.h); + // THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them + // as the descriptors themselves, so a word that does not unpack is taken to + // be one of those rather than being refused. + const int use = (fd >= 0) ? fd : static_cast(s.h); + + if (const int rc = await(use, static_cast(okm::poll_in), ns); rc != kal_ok) + return { 0, rc }; + return kal_stream_read(s, buf, len); +} + +kal_io_result kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return { 0, kal_ok }; + + const int fd = okm::unpack(s.h); + const int use = (fd >= 0) ? fd : static_cast(s.h); + + if (const int rc = await(use, static_cast(okm::poll_out), ns); rc != kal_ok) + return { 0, rc }; + return kal_stream_write(s, buf, len); +} + +int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = okm::unpack(l.h); + if (fd < 0) return kal_err_invalid; + + if (const int rc = await(fd, static_cast(okm::poll_in), ns); rc != kal_ok) + return rc; + return kal_net_accept(l, out); +} + +kal_io_result kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from, kal_u64 ns) { + const int fd = okm::unpack(d.h); + if (fd < 0) return { 0, kal_err_invalid }; + + if (const int rc = await(fd, static_cast(okm::poll_in), ns); rc != kal_ok) + return { 0, rc }; + return kal_datagram_recv_from(d, buf, len, from); +} + +int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* terminated) { + if (p.h == 0) return kal_err_invalid; + + // WNOHANG AND A POLLING LOOP, BECAUSE THIS KERNEL HAS NO BOUNDED WAIT FOR A + // CHILD EITHER. `wait4' blocks or does not wait at all; there is no bound. + // + // The alternative is a handler for the signal a child's end raises, which + // is process-wide state: an implementation that installed one would take a + // facility away from the program above it. The loop is what the environment + // permits, and the interval is the granularity this interface reports so + // that the polling cost is stated rather than hidden. + constexpr okm_u64 interval_ns = 1000000ull; // one millisecond + okm_u64 waited = 0; + + for (;;) { + int st = 0; + const okm_long r = okm::sys(okm::nr_wait4, static_cast(p.h), + reinterpret_cast(&st), + okm::wnohang, 0); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return okm::translate(r); + + if (r != 0) { + const int signalled = st & 0x7f; + if (signalled == 0) { + if (status) *status = (st >> 8) & 0xff; + if (terminated) *terminated = 0; + } else { + if (status) *status = signalled; + if (terminated) *terminated = 1; + } + return kal_ok; + } + + if (ns != 0 && waited >= ns) return kal_err_again; + + // openkal.time's own suspension, rather than a second spelling of it. + // This implementation provides that interface and time.cpp records what + // this system's suspension is expressed with; reproducing it here would + // be the same decision written twice. + kal_time_sleep(interval_ns); + waited += interval_ns; + } +} + +// The bound this kernel distinguishes. `poll' takes milliseconds and there is +// no call here that takes less, so a millisecond is what an implementation can +// honestly report --- and it is also the interval the child-waiting loop above +// polls at, so a caller asking for less is not told a number one of the +// operations cannot meet. +const kal_uintptr kal_timeout_granularity_ns = 1000000u; + +} // extern "C" From 1ffa4ebf0a935b3591922290f054c57e3fe8f562 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 21:31:46 +0800 Subject: [PATCH 2/3] Record why the instruction-cache maintenance differs between the three --- src/exec.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/exec.cpp b/src/exec.cpp index 016c3bf..42820cb 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -35,11 +35,17 @@ // through the instruction path, and nothing in `mprotect' makes the second path // see the first's writes. The specification places the maintenance upon the // PROGRAM --- the conformance suite performs it itself and says why: the -// program is the party that knows which bytes it wrote --- and this -// implementation performs it too, over the region whose protection it just -// changed. A program that also does it pays for one redundant sequence; a -// program that does not is not silently wrong on one of this system's two -// architectures. On x86_64 the builtin expands to nothing. +// program is the party that knows which bytes it wrote --- so what happens here +// is help rather than conformance, and a program that also does it pays for one +// redundant sequence. +// +// ⭐ IT IS DONE HERE AND NOT IN openkal-linux, WHICH IS AN ASYMMETRY WITH A +// REASON RATHER THAN AN OVERSIGHT. On this system's two architectures the +// builtin expands to nothing (x86_64) or to the maintenance sequence inline +// (aarch64), so it costs nothing to link. On riscv64 it becomes a CALL into the +// compiler's support library, and openkal-linux declines to acquire that +// dependency for an operation the specification does not require of it. +// openkal-linux/src/exec.cpp records the same reasoning from the other side. namespace { From ddfed933a614db2a232a01cb19489ecc01e34a83 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 21:45:57 +0800 Subject: [PATCH 3/3] Do not invalidate the instruction cache: the builtin is a call here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `__builtin___clear_cache' was added to `kal_exec_publish' on the reading that it expands to nothing on x86_64 and to the maintenance sequence INLINE on aarch64. The second half was false, and this package's own independence check said so: target/aarch64-macos/…/obj/exec.o references a symbol it must not: ___clear_cache The builtin becomes a CALL into the compiler's support library on this architecture, exactly as it does on riscv64. This implementation is reachable from a program that carries no other runtime and that check exists to keep it so, so the dependency is not available here. ⭐ Nothing is lost: the specification places the maintenance upon the PROGRAM, and the conformance suite performs it itself and says why. What the three implementations now share is a rule rather than an accident --- an implementation performs it where its environment offers it as a call of the environment's own (openkal-windows has `FlushInstructionCache'), and does not where the only means is a compiler builtin that becomes a dependency upon the compiler's support library. --- README.md | 2 +- src/exec.cpp | 50 +++++++++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 0ad3242..7929863 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ and each is provided in whole: | `openkal.datagram` | the same calls, with `SOCK_DGRAM` | | `openkal.timeout` | `poll`, whose bound is stated in milliseconds. `ppoll` does not exist here, so a millisecond is the granularity this implementation reports — what the environment can distinguish rather than what would be convenient | | `openkal.space` | the duplication primitive `openkal.process` was already built on. ⚠️ The duplicate is distinguished by the call's **second** return value: both images receive a process identifier in the first, so an implementation testing that one against zero would decide that neither image was the duplicate | -| `openkal.exec` | a mapping obtained writable and made executable afterwards, which is the only order this system permits | +| `openkal.exec` | a mapping obtained writable and made executable afterwards, which is the only order this system permits. ⚠️ The instruction-cache maintenance is **not** performed here: `__builtin___clear_cache` becomes a call to `___clear_cache` in the compiler's support library on `arm64`, and this implementation is reachable from a program that carries no other runtime. The specification places that maintenance upon the program | **⚠️ Clause 6.5 names this system and `openkal.exec` in terms**, and says availability may be settled by how the artifact is *produced* — a signed diff --git a/src/exec.cpp b/src/exec.cpp index 42820cb..34625d7 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -29,23 +29,38 @@ // resolution, together with whatever produces the artifact. The test is here so // that the question is answered by the system rather than by this comment. // -// ⚠️ THE INSTRUCTION CACHE IS INVALIDATED HERE AS WELL, AND THAT DOES NOT MOVE -// THE OBLIGATION. A processor with separate caches for data and instructions -// has just had bytes written through the data path that it is about to fetch -// through the instruction path, and nothing in `mprotect' makes the second path -// see the first's writes. The specification places the maintenance upon the -// PROGRAM --- the conformance suite performs it itself and says why: the -// program is the party that knows which bytes it wrote --- so what happens here -// is help rather than conformance, and a program that also does it pays for one -// redundant sequence. +// ⚠️⚠️ THE INSTRUCTION CACHE IS NOT INVALIDATED HERE, AND ONE VERSION OF THIS +// FILE DID INVALIDATE IT. THE MEASUREMENT IS WHY IT DOES NOT. // -// ⭐ IT IS DONE HERE AND NOT IN openkal-linux, WHICH IS AN ASYMMETRY WITH A -// REASON RATHER THAN AN OVERSIGHT. On this system's two architectures the -// builtin expands to nothing (x86_64) or to the maintenance sequence inline -// (aarch64), so it costs nothing to link. On riscv64 it becomes a CALL into the -// compiler's support library, and openkal-linux declines to acquire that -// dependency for an operation the specification does not require of it. -// openkal-linux/src/exec.cpp records the same reasoning from the other side. +// A processor with separate caches for data and instructions has just had bytes +// written through the data path that it is about to fetch through the +// instruction path, and nothing in `mprotect' makes the second path see the +// first's writes. So `__builtin___clear_cache' looked like the right thing to +// add, on the reading that it expands to nothing on x86_64 and to the +// maintenance sequence INLINE on aarch64. +// +// ⚠️ The second half of that reading was false, and this package's own +// independence check said so within the hour: +// +// target/aarch64-macos/…/obj/exec.o references a symbol it must not: +// ___clear_cache +// +// The builtin becomes a CALL into the compiler's support library on this +// architecture, exactly as it does on riscv64. This implementation is reachable +// from a program that carries no other runtime and the check exists to keep it +// that way, so acquiring that dependency is not available here. +// +// ⭐ THE SPECIFICATION PLACES THE MAINTENANCE UPON THE PROGRAM in any case --- the +// conformance suite performs it itself and says why: the program is the party +// that knows which bytes it wrote. So nothing is lost by not doing it, and what +// the three implementations now share is a rule rather than an accident: +// +// an implementation performs the maintenance where its environment offers +// it as a CALL of the environment's own (openkal-windows has +// `FlushInstructionCache'), and does not where the only means is a compiler +// builtin that becomes a dependency upon the compiler's support library. +// +// openkal-linux/src/exec.cpp records the same rule from the other side. namespace { @@ -79,9 +94,6 @@ int kal_exec_publish(void* p, kal_uintptr size) { static_cast(bytes), okm::prot_read | okm::prot_exec); if (okm::failed(r)) return okm::translate(r); - - auto* begin = static_cast(p); - __builtin___clear_cache(begin, begin + bytes); return kal_ok; }