Skip to content

Commit 7b65854

Browse files
0.6.0 — adopt openkal 0.9, and ask this machine what its page is (#14)
* 0.6.0 --- adopt openkal 0.9, and ask this machine what its page is ⚠️⚠️ THE ALLOCATOR ASSUMED A FOUR-KILOBYTE PAGE ON A SYSTEM WITH TWO. `kPage' was the constant 4096, and this system's own hardware pages are four kilobytes on one architecture and SIXTEEN on the other. Allocation appeared to work, because a mapping rounded to four kilobytes is rounded up again by the kernel --- while `kal_free' unmapped a range SHORTER than the one mapped, and the remainder was never returned. The page is now asked for once, through `hw.pagesize', and `kal_memory_granularity' reports it. The rest follows the specification: * `kal_fs_props' takes the directory and consults the format the volume is, which this kernel names in words. A word per implementation could state none of its positions honestly here: the volume this system is ordinarily installed on compares names without regard to case and a volume attached to the same machine may not, and both are reachable through the preopen this implementation supplies; * asking now resolves a link, because opening always did; * transfers return one signed word; the parameters and names are copied into the caller's buffer and the length reported is the value's own; * `kal_node_info' carries its own size, reports what was filled, and carries the device and inode as an opaque identity; * `kal_fs_link_create' and `kal_fs_link_read' over symlinkat and readlinkat; * `kal_fs_max_name'; typed stream handles; `kal_version' and `kal_interfaces'. Ninety-five names are exported and none other, checked against SURFACE.txt. * README: the versions it names are the versions that exist Every README here opens by showing what a program writes in its manifest, which is the first thing a reader copies and the last thing anyone edits. These lines had drifted --- the specification's own README asked for a version four minor releases old --- and nothing checked them. `openkal/tools/check-readme-versions.sh` now does. * fix: measure whether this system grants executable memory src/exec.cpp 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, reasoned that executable memory is granted only to an artifact produced with one and returned zero from kal_exec_props. The operations behaved as the first said and the capability word said the second. The disagreement was invisible while every consumer was statically linked -- such a consumer never asks, it links the operations and uses them. It became load-bearing when an implementation's own account of itself became 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. Neither comment is the party that knows. Whether this system grants executable memory depends on how the artifact was signed, which is settled after this code is compiled. So the enquiry performs the thing it is asked about -- one reservation, one publish, one release -- and reports what the kernel said; kal_exec_alloc declines when it reports no. The two can no longer disagree. kPage = 4096 is also gone. The argument for it held for the reservation and failed for the release: munmap with a length shorter than the mapping unmaps less than was mapped, which is the defect src/memory.cpp records having measured on this same system one file away. * ci: substitute the specification per step, not per job Measured 2026-08-28 across the eight repositories of this ecosystem while one change spanned all of them: eight jobs in four of them called `mcpp build' at a point where the manifest still named openkal BY VERSION, so a version under review -- which by definition is not published -- failed them with E_NOT_FOUND. The mechanism is not a missing substitution. run-conformance.sh substitutes the manifest and RESTORES IT ON EXIT, correctly; every step after it is back to naming a version. So an audit asking "does this job substitute?" passes the job and misses the steps, which is how the first pass at this found three repositories and not four. These steps are green on main and can only be green there, because there the published version is the one under test. It is not a check that fails, it is a check that cannot run at the only time it would have something to say. The substitution is also portable now: the opensbi and uefi portability jobs run on macOS and Windows, where BSD sed requires an argument to -i that GNU sed refuses. --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent 04fba62 commit 7b65854

22 files changed

Lines changed: 504 additions & 160 deletions

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,39 @@ jobs:
182182
run: |
183183
bash .spec/tools/run-conformance.sh openkal-macos . full,optional
184184
185+
# ⚠️⚠️ CLONING THE SPECIFICATION IS NOT THE SAME AS BUILDING AGAINST IT.
186+
#
187+
# `.spec` is cloned at the top of this job and consumed by the script
188+
# above, which substitutes this manifest itself and RESTORES IT ON EXIT
189+
# --- correctly, since a script that rewrote a checked-in file and walked
190+
# away would leave the tree holding a path. But every step BELOW calls
191+
# `mcpp build` directly, and by then the manifest names `openkal` by
192+
# version again, so they resolved the PUBLISHED specification:
193+
#
194+
# E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the synced
195+
# index ... the index is current, so this name is either wrong or not
196+
# published yet
197+
#
198+
# ⭐⭐ THE UNIT IS THE STEP, NOT THE JOB, AND NOT THE REPOSITORY. Measured
199+
# 2026-08-28 across the eight repositories of this ecosystem: eight jobs
200+
# in four of them had this shape. An audit that asked "does this job
201+
# substitute?" passed this one, because it does --- and then gives it
202+
# back. These steps are green on `main` and can only be green there,
203+
# because there the published version IS the one under test.
204+
- name: Point at the specification's working tree
205+
run: |
206+
set -euo pipefail
207+
# ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and
208+
# BSD sed requires an argument to -i that GNU sed refuses. A temporary
209+
# file is the spelling that holds on all three.
210+
subst() { # subst <file> <relative-path-to-the-specification>
211+
sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next"
212+
mv "$1.next" "$1"
213+
grep -q "path = \"$2\"" "$1" \
214+
|| { echo "::error::$1 was not substituted"; exit 1; }
215+
}
216+
subst mcpp.toml .spec
217+
185218
# The other architecture, as far as this system allows it to be reached.
186219
#
187220
# The system-call numbers agree between the two --- measured, in the

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ written on the kernel's own calls.
55

66
```toml
77
[dependencies]
8-
openkal = "0.8.0"
8+
openkal = "0.9.0"
99

1010
[target.'cfg(os = "macos")'.dependencies]
11-
openkal-macos = "0.5.0"
11+
openkal-macos = "0.6.0"
1212
```
1313

1414
Its purpose is as much to test the specification as to be used. A specification

mcpp.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-macos"
4-
version = "0.5.0"
4+
version = "0.6.0"
55
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."
66
license = "Apache-2.0"
77

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

2020
[dependencies]
21-
openkal = "0.8.0"
21+
openkal = "0.9.0"
2222

2323
[build]
2424
# The flags are attached to this package's own sources rather than to the whole

src/datagram.cpp

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) {
7272
return okm::from_kernel(ss, *out);
7373
}
7474

75-
kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len,
76-
const kal_endpoint* to) {
75+
kal_intptr kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len,
76+
const kal_endpoint* to) {
7777
const int fd = fd_of(d);
78-
if (fd < 0 || to == nullptr) return { 0, kal_err_invalid };
78+
if (fd < 0 || to == nullptr) return -kal_err_invalid;
7979

8080
okm::ksockaddr_storage ss{};
8181
okm_u32 addrlen = 0;
8282
if (const int rc = okm::to_kernel(*to, ss, addrlen); rc != kal_ok)
83-
return { 0, rc };
83+
return -rc;
8484

8585
for (;;) {
8686
const okm_long r = okm::sys(okm::nr_sendto, fd,
@@ -89,7 +89,7 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr
8989
reinterpret_cast<okm_long>(&ss),
9090
static_cast<okm_long>(addrlen));
9191
if (okm::interrupted(r)) continue;
92-
if (okm::failed(r)) return { 0, okm::translate(r) };
92+
if (okm::failed(r)) return -okm::translate(r);
9393

9494
// A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface
9595
// states. The kernel reports a count anyway; a count short of the length
@@ -98,14 +98,14 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr
9898
// a caller a partial send this interface says cannot occur, so it is
9999
// reported as a failure of the medium instead.
100100
const kal_uintptr n = static_cast<kal_uintptr>(r);
101-
return { n, n == len ? kal_ok : kal_err_io };
101+
return n == len ? static_cast<kal_intptr>(n) : -kal_err_io;
102102
}
103103
}
104104

105-
kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
106-
kal_endpoint* from) {
105+
kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
106+
kal_endpoint* from) {
107107
const int fd = fd_of(d);
108-
if (fd < 0) return { 0, kal_err_invalid };
108+
if (fd < 0) return -kal_err_invalid;
109109

110110
okm::ksockaddr_storage ss{};
111111
okm_u32 addrlen = static_cast<okm_u32>(sizeof ss);
@@ -117,7 +117,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
117117
reinterpret_cast<okm_long>(&ss),
118118
reinterpret_cast<okm_long>(&addrlen));
119119
if (okm::interrupted(r)) continue;
120-
if (okm::failed(r)) return { 0, okm::translate(r) };
120+
if (okm::failed(r)) return -okm::translate(r);
121121

122122
// THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was
123123
// sent. Without MSG_TRUNC the kernel already reports the former, which
@@ -133,7 +133,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
133133
from->port = 0;
134134
}
135135
}
136-
return { static_cast<kal_uintptr>(r), kal_ok };
136+
return static_cast<kal_intptr>(r);
137137
}
138138
}
139139

@@ -148,6 +148,6 @@ void kal_datagram_close(kal_datagram d) {
148148
// been set, and this interface has no operation that would set it; a word
149149
// claiming a facility no operation reaches is the disagreement clause 6.2 exists
150150
// to prevent.
151-
const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6;
151+
kal_uintptr kal_datagram_props(void) { return KAL_DGRAM_PROP_IPV6; }
152152

153153
} // extern "C"

src/env.cpp

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,56 @@ extern "C" {
2727

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

30-
const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) {
31-
if (index >= static_cast<kal_uintptr>(okm::g_argc)) { if (len) *len = 0; return nullptr; }
30+
// EVERY VALUE IS COPIED INTO THE CALLER'S BUFFER. These answered with a pointer
31+
// into this implementation's own storage, which is meaningful only while the
32+
// implementation shares the caller's address space. Each reports the length the
33+
// value HAS, so a caller with a large enough buffer is done in one call and one
34+
// that wants to size first passes a capacity of zero.
35+
namespace {
36+
kal_intptr give(const char* v, kal_uintptr n, char* out, kal_uintptr cap) {
37+
if (out != nullptr && cap != 0) okm::copy(out, v, n < cap ? n : cap);
38+
return static_cast<kal_intptr>(n);
39+
}
40+
} // namespace
41+
42+
kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) {
43+
if (index >= static_cast<kal_uintptr>(okm::g_argc)) return -kal_err_not_found;
3244
const char* s = okm::g_argv[index];
33-
if (len) *len = okm::length(s);
34-
return s;
45+
return give(s, okm::length(s), out, cap);
3546
}
3647

37-
const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len) {
48+
kal_intptr kal_env_var(const char* name, kal_uintptr name_len,
49+
char* out, kal_uintptr cap) {
50+
if (name == nullptr) return -kal_err_invalid;
3851
for (char** e = okm::g_envp; e && *e; ++e) {
3952
const char* entry = *e;
4053
kal_uintptr i = 0;
4154
while (i < name_len && entry[i] != '\0' && entry[i] == name[i]) ++i;
4255
if (i == name_len && entry[i] == '=') {
4356
const char* v = entry + name_len + 1;
44-
if (value_len) *value_len = okm::length(v);
45-
return v;
57+
return give(v, okm::length(v), out, cap);
4658
}
4759
}
48-
if (value_len) *value_len = 0;
49-
return nullptr;
60+
// A name that is not there is distinct from one whose value is empty.
61+
return -kal_err_not_found;
5062
}
5163

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

56-
const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len,
57-
const char** value, kal_uintptr* value_len) {
68+
// The NAME at a position. The value is then obtained by kal_env_var: an
69+
// operation answering both needs two buffers, two capacities and two lengths,
70+
// and its second half is kal_env_var written again.
71+
kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) {
5872
kal_uintptr n = 0;
5973
for (char** e = okm::g_envp; e && *e; ++e, ++n) {
6074
if (n != index) continue;
6175
const char* entry = *e;
6276
kal_uintptr i = 0; while (entry[i] != '\0' && entry[i] != '=') ++i;
63-
if (name_len) *name_len = i;
64-
const char* v = entry[i] == '=' ? entry + i + 1 : entry + i;
65-
if (value) *value = v;
66-
if (value_len) *value_len = okm::length(v);
67-
return entry;
77+
return give(entry, i, out, cap);
6878
}
69-
return nullptr;
79+
return -kal_err_not_found;
7080
}
7181

7282
}

src/exec.cpp

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "sys.h"
22
#include <openkal/exec.h>
3+
#include <openkal/memory.h>
34

45
// openkal.exec on this system.
56
//
@@ -74,22 +75,83 @@
7475

7576
namespace {
7677

77-
constexpr okm_uptr kPage = 4096;
78-
79-
// ⚠️ THE PAGE IS 16384 BYTES ON ONE OF THIS SYSTEM'S TWO ARCHITECTURES. Rounding
80-
// to the smaller number is still correct --- the kernel rounds up to its own
81-
// granularity, and a region reserved as 4096 occupies a whole page of whatever
82-
// size --- but a caller freeing with the size it reserved must reach the same
83-
// number, which it does because both go through the same rounding.
78+
// ⚠️ THE GRANULARITY IS ASKED FOR RATHER THAN ASSUMED. This file held
79+
// `constexpr okm_uptr kPage = 4096' and a comment arguing that rounding to the
80+
// smaller of this system's two page sizes was still correct because the kernel
81+
// rounds up. The argument holds for the reservation and fails for the release:
82+
// `munmap' with a length shorter than the mapping unmaps less than was mapped.
83+
// It is the same defect `src/memory.cpp' records having measured, in the same
84+
// system, one file away --- so the number now comes from the one operation
85+
// that answers it.
8486
okm_uptr round_up(okm_uptr n, okm_uptr to) { return (n + to - 1) & ~(to - 1); }
8587

88+
okm_uptr granularity() {
89+
return static_cast<okm_uptr>(kal_memory_granularity());
90+
}
91+
92+
// ⭐⭐ WHETHER THIS SYSTEM GRANTS EXECUTABLE MEMORY IS MEASURED, NOT ARGUED.
93+
//
94+
// This file previously carried both answers. One comment reasoned that the
95+
// write-then-publish order is the case an entitlement is NOT needed for and
96+
// concluded the interface is provided unconditionally; another, thirty lines
97+
// below it, reasoned that executable memory is granted only to an artifact
98+
// produced with an entitlement and returned zero. The operations behaved as
99+
// the first said and the capability word said the second.
100+
//
101+
// ⚠️ AND THE DISAGREEMENT WAS INVISIBLE UNTIL A CONSUMER COULD READ THE WORD.
102+
// A statically-linked caller never asked: it linked the operations and used
103+
// them. The word became load-bearing when the specification made an
104+
// implementation's own account of itself part of the ABI, and the conformance
105+
// suite then reported what had been true all along --- `an implementation that
106+
// does not claim availability reserves nothing' DID NOT HOLD, because this one
107+
// claimed nothing and reserved anyway.
108+
//
109+
// The remedy is not to pick the more likely of the two readings. It is that
110+
// neither this file nor any comment in it is the party that knows: the answer
111+
// depends on how the artifact was signed, which is settled after this code is
112+
// compiled and can differ between two runs of the same binary. So the enquiry
113+
// performs the thing it is being asked about --- one reservation, one publish,
114+
// one release --- and reports what the kernel said.
115+
//
116+
// The probe is the operation's own path, so an environment where publishing
117+
// fails is one where this reports unavailable and `kal_exec_alloc' declines,
118+
// and the two can no longer disagree.
119+
int probe() {
120+
const okm_uptr bytes = granularity();
121+
const okm_long m = okm::sys(okm::nr_mmap, 0, static_cast<okm_long>(bytes),
122+
okm::prot_read | okm::prot_write,
123+
okm::map_private | okm::map_anon, -1, 0);
124+
if (okm::failed(m)) return 2;
125+
const okm_long p = okm::sys(okm::nr_mprotect, m,
126+
static_cast<okm_long>(bytes),
127+
okm::prot_read | okm::prot_exec);
128+
okm::sys(okm::nr_munmap, m, static_cast<okm_long>(bytes));
129+
return okm::failed(p) ? 2 : 1;
130+
}
131+
132+
// Asked once. Constant-initialised, so no guard variable is emitted and this
133+
// file acquires no dependency upon the runtime --- the property the
134+
// independence check in this package exists to hold. Two contexts racing here
135+
// perform the probe twice and store the same answer.
136+
bool available() {
137+
static int cached = 0;
138+
int v = __atomic_load_n(&cached, __ATOMIC_RELAXED);
139+
if (v == 0) { v = probe(); __atomic_store_n(&cached, v, __ATOMIC_RELAXED); }
140+
return v == 1;
141+
}
142+
86143
} // namespace
87144

88145
extern "C" {
89146

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

100162
int kal_exec_publish(void* p, kal_uintptr size) {
101163
if (p == nullptr || size == 0) return kal_err_invalid;
102-
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), kPage);
164+
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), granularity());
103165
const okm_long r = okm::sys(okm::nr_mprotect, reinterpret_cast<okm_long>(p),
104166
static_cast<okm_long>(bytes),
105167
okm::prot_read | okm::prot_exec);
@@ -109,17 +171,19 @@ int kal_exec_publish(void* p, kal_uintptr size) {
109171

110172
void kal_exec_free(void* p, kal_uintptr size) {
111173
if (p == nullptr || size == 0) return;
112-
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), kPage);
174+
const okm_uptr bytes = round_up(static_cast<okm_uptr>(size), granularity());
113175
okm::sys(okm::nr_munmap, reinterpret_cast<okm_long>(p),
114176
static_cast<okm_long>(bytes));
115177
}
116178

117179
// A published region may NOT be reserved for writing again on this system, and
118-
// the position is withheld accordingly. Asking this kernel to make an executable
119-
// mapping writable is the case it refuses, which is the whole reason the
120-
// interface separates the two states; a caller that must change published bytes
121-
// reserves a second region and abandons the first, which is what the header
122-
// says a zero here means.
123-
const kal_uintptr kal_exec_props = 0;
180+
// the position is withheld accordingly. Asking this kernel to make an
181+
// executable mapping writable is the case it refuses, which is the whole reason
182+
// the interface separates the two states; a caller that must change published
183+
// bytes reserves a second region and abandons the first, which is what the
184+
// header says a zero in that position means.
185+
kal_uintptr kal_exec_props(void) {
186+
return available() ? KAL_EXEC_PROP_AVAILABLE : 0;
187+
}
124188

125189
} // extern "C"

0 commit comments

Comments
 (0)