Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,39 @@ jobs:
run: |
bash .spec/tools/run-conformance.sh openkal-macos . full,optional

# ⚠️⚠️ CLONING THE SPECIFICATION IS NOT THE SAME AS BUILDING AGAINST IT.
#
# `.spec` is cloned at the top of this job and consumed by the script
# above, which substitutes this manifest itself and RESTORES IT ON EXIT
# --- correctly, since a script that rewrote a checked-in file and walked
# away would leave the tree holding a path. But every step BELOW calls
# `mcpp build` directly, and by then the manifest names `openkal` by
# version again, so they resolved the PUBLISHED specification:
#
# E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the synced
# index ... the index is current, so this name is either wrong or not
# published yet
#
# ⭐⭐ THE UNIT IS THE STEP, NOT THE JOB, AND NOT THE REPOSITORY. Measured
# 2026-08-28 across the eight repositories of this ecosystem: eight jobs
# in four of them had this shape. An audit that asked "does this job
# substitute?" passed this one, because it does --- and then gives it
# back. These steps are green on `main` and can only be green there,
# because there the published version IS the one under test.
- name: Point at the specification's working tree
run: |
set -euo pipefail
# ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and
# BSD sed requires an argument to -i that GNU sed refuses. A temporary
# file is the spelling that holds on all three.
subst() { # subst <file> <relative-path-to-the-specification>
sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next"
mv "$1.next" "$1"
grep -q "path = \"$2\"" "$1" \
|| { echo "::error::$1 was not substituted"; exit 1; }
}
subst mcpp.toml .spec

# The other architecture, as far as this system allows it to be reached.
#
# The system-call numbers agree between the two --- measured, in the
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ written on the kernel's own calls.

```toml
[dependencies]
openkal = "0.8.0"
openkal = "0.9.0"

[target.'cfg(os = "macos")'.dependencies]
openkal-macos = "0.5.0"
openkal-macos = "0.6.0"
```

Its purpose is as much to test the specification as to be used. A specification
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-macos"
version = "0.5.0"
version = "0.6.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"

Expand All @@ -18,7 +18,7 @@ authors = ["mcpplibs"]
repo = "https://github.com/mcpplibs/openkal-macos"

[dependencies]
openkal = "0.8.0"
openkal = "0.9.0"

[build]
# The flags are attached to this package's own sources rather than to the whole
Expand Down
24 changes: 12 additions & 12 deletions src/datagram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) {
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) {
kal_intptr 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 };
if (fd < 0 || to == nullptr) return -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 };
return -rc;

for (;;) {
const okm_long r = okm::sys(okm::nr_sendto, fd,
Expand All @@ -89,7 +89,7 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr
reinterpret_cast<okm_long>(&ss),
static_cast<okm_long>(addrlen));
if (okm::interrupted(r)) continue;
if (okm::failed(r)) return { 0, okm::translate(r) };
if (okm::failed(r)) return -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
Expand All @@ -98,14 +98,14 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr
// 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<kal_uintptr>(r);
return { n, n == len ? kal_ok : kal_err_io };
return n == len ? static_cast<kal_intptr>(n) : -kal_err_io;
}
}

kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
kal_endpoint* from) {
kal_intptr 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 };
if (fd < 0) return -kal_err_invalid;

okm::ksockaddr_storage ss{};
okm_u32 addrlen = static_cast<okm_u32>(sizeof ss);
Expand All @@ -117,7 +117,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
reinterpret_cast<okm_long>(&ss),
reinterpret_cast<okm_long>(&addrlen));
if (okm::interrupted(r)) continue;
if (okm::failed(r)) return { 0, okm::translate(r) };
if (okm::failed(r)) return -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
Expand All @@ -133,7 +133,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
from->port = 0;
}
}
return { static_cast<kal_uintptr>(r), kal_ok };
return static_cast<kal_intptr>(r);
}
}

Expand All @@ -148,6 +148,6 @@ void kal_datagram_close(kal_datagram d) {
// 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;
kal_uintptr kal_datagram_props(void) { return KAL_DGRAM_PROP_IPV6; }

} // extern "C"
44 changes: 27 additions & 17 deletions src/env.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,46 +27,56 @@ extern "C" {

kal_uintptr kal_env_arg_count(void) { return static_cast<kal_uintptr>(okm::g_argc); }

const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) {
if (index >= static_cast<kal_uintptr>(okm::g_argc)) { if (len) *len = 0; return nullptr; }
// EVERY VALUE IS COPIED INTO THE CALLER'S BUFFER. These answered with a pointer
// into this implementation's own storage, which is meaningful only while the
// implementation shares the caller's address space. Each reports the length the
// value HAS, so a caller with a large enough buffer is done in one call and one
// that wants to size first passes a capacity of zero.
namespace {
kal_intptr give(const char* v, kal_uintptr n, char* out, kal_uintptr cap) {
if (out != nullptr && cap != 0) okm::copy(out, v, n < cap ? n : cap);
return static_cast<kal_intptr>(n);
}
} // namespace

kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) {
if (index >= static_cast<kal_uintptr>(okm::g_argc)) return -kal_err_not_found;
const char* s = okm::g_argv[index];
if (len) *len = okm::length(s);
return s;
return give(s, okm::length(s), out, cap);
}

const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len) {
kal_intptr kal_env_var(const char* name, kal_uintptr name_len,
char* out, kal_uintptr cap) {
if (name == nullptr) return -kal_err_invalid;
for (char** e = okm::g_envp; e && *e; ++e) {
const char* entry = *e;
kal_uintptr i = 0;
while (i < name_len && entry[i] != '\0' && entry[i] == name[i]) ++i;
if (i == name_len && entry[i] == '=') {
const char* v = entry + name_len + 1;
if (value_len) *value_len = okm::length(v);
return v;
return give(v, okm::length(v), out, cap);
}
}
if (value_len) *value_len = 0;
return nullptr;
// A name that is not there is distinct from one whose value is empty.
return -kal_err_not_found;
}

kal_uintptr kal_env_var_count(void) {
kal_uintptr n = 0; for (char** e = okm::g_envp; e && *e; ++e) ++n; return n;
}

const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len,
const char** value, kal_uintptr* value_len) {
// The NAME at a position. The value is then obtained by kal_env_var: an
// operation answering both needs two buffers, two capacities and two lengths,
// and its second half is kal_env_var written again.
kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) {
kal_uintptr n = 0;
for (char** e = okm::g_envp; e && *e; ++e, ++n) {
if (n != index) continue;
const char* entry = *e;
kal_uintptr i = 0; while (entry[i] != '\0' && entry[i] != '=') ++i;
if (name_len) *name_len = i;
const char* v = entry[i] == '=' ? entry + i + 1 : entry + i;
if (value) *value = v;
if (value_len) *value_len = okm::length(v);
return entry;
return give(entry, i, out, cap);
}
return nullptr;
return -kal_err_not_found;
}

}
96 changes: 80 additions & 16 deletions src/exec.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "sys.h"
#include <openkal/exec.h>
#include <openkal/memory.h>

// openkal.exec on this system.
//
Expand Down Expand Up @@ -74,22 +75,83 @@

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.
// ⚠️ THE GRANULARITY IS ASKED FOR RATHER THAN ASSUMED. This file held
// `constexpr okm_uptr kPage = 4096' and a comment arguing that rounding to the
// smaller of this system's two page sizes was still correct because the kernel
// rounds up. The argument holds for the reservation and fails for the release:
// `munmap' with a length shorter than the mapping unmaps less than was mapped.
// It is the same defect `src/memory.cpp' records having measured, in the same
// system, one file away --- so the number now comes from the one operation
// that answers it.
okm_uptr round_up(okm_uptr n, okm_uptr to) { return (n + to - 1) & ~(to - 1); }

okm_uptr granularity() {
return static_cast<okm_uptr>(kal_memory_granularity());
}

// ⭐⭐ WHETHER THIS SYSTEM GRANTS EXECUTABLE MEMORY IS MEASURED, NOT ARGUED.
//
// This file previously carried both answers. One comment reasoned that the
// write-then-publish order is the case an entitlement is NOT needed for and
// concluded the interface is provided unconditionally; another, thirty lines
// below it, reasoned that executable memory is granted only to an artifact
// produced with an entitlement and returned zero. The operations behaved as
// the first said and the capability word said the second.
//
// ⚠️ AND THE DISAGREEMENT WAS INVISIBLE UNTIL A CONSUMER COULD READ THE WORD.
// A statically-linked caller never asked: it linked the operations and used
// them. The word became load-bearing when the specification made an
// implementation's own account of itself part of the ABI, and the conformance
// suite then reported what had been true all along --- `an implementation that
// does not claim availability reserves nothing' DID NOT HOLD, because this one
// claimed nothing and reserved anyway.
//
// The remedy is not to pick the more likely of the two readings. It is that
// neither this file nor any comment in it is the party that knows: the answer
// depends on how the artifact was signed, which is settled after this code is
// compiled and can differ between two runs of the same binary. So the enquiry
// performs the thing it is being asked about --- one reservation, one publish,
// one release --- and reports what the kernel said.
//
// The probe is the operation's own path, so an environment where publishing
// fails is one where this reports unavailable and `kal_exec_alloc' declines,
// and the two can no longer disagree.
int probe() {
const okm_uptr bytes = granularity();
const okm_long m = okm::sys(okm::nr_mmap, 0, static_cast<okm_long>(bytes),
okm::prot_read | okm::prot_write,
okm::map_private | okm::map_anon, -1, 0);
if (okm::failed(m)) return 2;
const okm_long p = okm::sys(okm::nr_mprotect, m,
static_cast<okm_long>(bytes),
okm::prot_read | okm::prot_exec);
okm::sys(okm::nr_munmap, m, static_cast<okm_long>(bytes));
return okm::failed(p) ? 2 : 1;
}

// Asked once. Constant-initialised, so no guard variable is emitted and this
// file acquires no dependency upon the runtime --- the property the
// independence check in this package exists to hold. Two contexts racing here
// perform the probe twice and store the same answer.
bool available() {
static int cached = 0;
int v = __atomic_load_n(&cached, __ATOMIC_RELAXED);
if (v == 0) { v = probe(); __atomic_store_n(&cached, v, __ATOMIC_RELAXED); }
return v == 1;
}

} // namespace

extern "C" {

void* kal_exec_alloc(kal_uintptr size) {
if (size == 0) return nullptr;
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), kPage);
// An implementation that does not claim availability reserves nothing.
// Otherwise the word is advice a caller cannot act upon: it would report
// unavailable and then hand back memory, and a caller that believed the
// word would have declined memory it could have had.
if (!available()) return nullptr;
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), granularity());
const okm_long r = okm::sys(okm::nr_mmap, 0, static_cast<okm_long>(bytes),
okm::prot_read | okm::prot_write,
okm::map_private | okm::map_anon, -1, 0);
Expand All @@ -99,7 +161,7 @@ void* kal_exec_alloc(kal_uintptr size) {

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<okm_uptr>(size), kPage);
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), granularity());
const okm_long r = okm::sys(okm::nr_mprotect, reinterpret_cast<okm_long>(p),
static_cast<okm_long>(bytes),
okm::prot_read | okm::prot_exec);
Expand All @@ -109,17 +171,19 @@ int kal_exec_publish(void* p, kal_uintptr size) {

void kal_exec_free(void* p, kal_uintptr size) {
if (p == nullptr || size == 0) return;
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), kPage);
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), granularity());
okm::sys(okm::nr_munmap, reinterpret_cast<okm_long>(p),
static_cast<okm_long>(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;
// 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 in that position means.
kal_uintptr kal_exec_props(void) {
return available() ? KAL_EXEC_PROP_AVAILABLE : 0;
}

} // extern "C"
Loading
Loading