From 03839101f625c450ce4886c85f8cf015e6074fd7 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 00:57:33 +0800 Subject: [PATCH 1/4] 0.6.0 --- adopt openkal 0.9, and ask this machine what its page is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ 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. --- mcpp.toml | 4 +- src/datagram.cpp | 24 +++--- src/env.cpp | 44 ++++++---- src/exec.cpp | 7 +- src/fs.cpp | 203 ++++++++++++++++++++++++++++++++++++++--------- src/memory.cpp | 53 ++++++++++++- src/net.cpp | 6 +- src/process.cpp | 16 ++-- src/random.cpp | 2 +- src/space.cpp | 4 +- src/stream.cpp | 16 ++-- src/sys.h | 30 ++++++- src/task.cpp | 4 +- src/time.cpp | 4 +- src/timeout.cpp | 22 ++--- src/version.cpp | 21 +++++ 16 files changed, 349 insertions(+), 111 deletions(-) create mode 100644 src/version.cpp diff --git a/mcpp.toml b/mcpp.toml index fc8a9c1..d2dbc96 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" @@ -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 diff --git a/src/datagram.cpp b/src/datagram.cpp index 1074ebc..3443122 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -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, @@ -89,7 +89,7 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr reinterpret_cast(&ss), static_cast(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 @@ -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(r); - return { n, n == len ? kal_ok : kal_err_io }; + return n == len ? static_cast(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(sizeof ss); @@ -117,7 +117,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, reinterpret_cast(&ss), reinterpret_cast(&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 @@ -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(r), kal_ok }; + return static_cast(r); } } @@ -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" diff --git a/src/env.cpp b/src/env.cpp index 224e0c1..87d7a6a 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -27,46 +27,56 @@ extern "C" { kal_uintptr kal_env_arg_count(void) { return static_cast(okm::g_argc); } -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) { - if (index >= static_cast(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(n); +} +} // namespace + +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { + if (index >= static_cast(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; } } diff --git a/src/exec.cpp b/src/exec.cpp index 994d3c3..e14061c 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -120,6 +120,11 @@ void kal_exec_free(void* p, kal_uintptr size) { // 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; +// Executable memory on this system is granted only to an artifact produced +// with the entitlement for it, which is a decision made after the link. The +// interface is provided and the position reports whether this artifact may use +// it --- clause 6.5's answer at dependency resolution cannot serve an artifact +// produced once and run in many environments. +kal_uintptr kal_exec_props(void) { return 0; } } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index e9e3959..b5ea7c6 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -64,14 +64,50 @@ int kind_of(okm_u32 mode) { } } -void fill_info(const okm::kstat64& st, kal_node_info* out) { - *out = kal_node_info{ - static_cast(st.size), - static_cast(st.mtime_sec) * 1000000000u - + static_cast(st.mtime_nsec), - kind_of(okm::stat_mode(st)), - (okm::stat_mode(st) & 0200u) != 0 ? 1 : 0, - }; +// Writes no more of the structure than the caller says exists on its side, and +// reports which fields it filled. `wanted' is ignored and every field is +// filled: one call answers all of them on this kernel, so selecting would cost +// a branch and save nothing. +void fill_info(const okm::kstat64& st, kal_u32 wanted, kal_node_info* out) { + (void)wanted; + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_ALL; + v.size = static_cast(st.size); + v.modified_ns = static_cast(st.mtime_sec) * 1000000000u + + static_cast(st.mtime_nsec); + // Opaque to a caller, which may compare it and may not read it. The device + // and the inode are this kernel's answer and not the interface's shape. + v.identity[0] = st.dev; + v.identity[1] = st.ino; + v.kind = kind_of(okm::stat_mode(st)); + v.writable = (okm::stat_mode(st) & 0200u) != 0 ? 1 : 0; + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + okm::copy(out, &v, n); +} + +void fill_absent(kal_node_info* out) { + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_KIND; + v.kind = kal_node_absent; + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + okm::copy(out, &v, n); +} + +// The caller must state how much of the structure exists on its side. +bool info_ok(const kal_node_info* out) { + return out != nullptr && out->self_size >= sizeof(kal_u32) * 2; +} + +// Copies a name into a caller's buffer and reports the length it HAS. +kal_uintptr put_name(const char* src, kal_uintptr n, + char* out, kal_uintptr cap, kal_uintptr* len) { + if (out != nullptr && cap != 0) okm::copy(out, src, n < cap ? n : cap); + if (len) *len = n; + return n; } // Enumeration reads the kernel's own directory records. It holds a descriptor @@ -92,14 +128,14 @@ extern "C" { kal_uintptr kal_fs_preopen_count(void) { kal_uintptr n = 0; table(&n); return n; } -int kal_fs_preopen(kal_uintptr index, kal_dir* out, const char** name, kal_uintptr* len) { +int kal_fs_preopen(kal_uintptr index, kal_dir* out, + char* name_out, kal_uintptr name_cap, kal_uintptr* name_len) { kal_uintptr n = 0; preopen* t = table(&n); if (index >= n || out == nullptr) return kal_err_invalid; if (t[index].handle == 0) return kal_err_permission; *out = kal_dir{ t[index].handle }; - if (name) *name = t[index].name; - if (len) *len = t[index].len; + put_name(t[index].name, t[index].len, name_out, name_cap, name_len); return kal_ok; } @@ -135,14 +171,6 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, return kal_ok; } -int kal_fs_open_file(kal_dir base, const char* name, kal_uintptr len, - int write, int create, kal_file* out) { - kal_uintptr flags = KAL_OPEN_READ; - if (write) flags |= KAL_OPEN_WRITE; - if (create) flags |= KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE; - return kal_fs_open(base, name, len, flags, out); -} - void kal_fs_close_dir(kal_dir d) { const int fd = okm::unpack(d.h); if (fd >= 0) { okm::retire(d.h); okm::sys(okm::nr_close, fd); } @@ -153,9 +181,11 @@ void kal_fs_close_file(kal_file f) { if (fd >= 0) { okm::retire(f.h); okm::sys(okm::nr_close, fd); } } -kal_uintptr kal_fs_stream(kal_file f) { +kal_uintptr kal_fs_max_name(void) { return okm::max_name; } + +kal_stream kal_fs_stream(kal_file f) { const int fd = okm::unpack(f.h); - return fd < 0 ? 0u : static_cast(fd); + return kal_stream{ fd < 0 ? 0u : static_cast(fd) }; } int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { @@ -177,34 +207,41 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okm::failed(r) ? okm::translate(r) : kal_ok; } -int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_node_info* out) { +int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, kal_u32 wanted, kal_node_info* out) { const int b = okm::unpack(base.h); - if (b < 0 || out == nullptr || !okm::acceptable(name, len)) return kal_err_invalid; + if (b < 0 || !info_ok(out) || !okm::acceptable(name, len)) return kal_err_invalid; okm::terminated t(name, len); if (!t.ok) return kal_err_invalid; okm::kstat64 st{}; + // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. + // This implementation asked with AT_SYMLINK_NOFOLLOW always while + // `kal_fs_open' resolved, so a name referring to a node whose content is + // another name was reported as that node while opening it reached a file. + const okm_long at = (flags & KAL_FS_NO_RESOLVE) ? okm::at_symlink_nofollow : 0; const okm_long r = okm::sys(okm::nr_fstatat64, b, reinterpret_cast(t.buf), - reinterpret_cast(&st), okm::at_symlink_nofollow); + reinterpret_cast(&st), at); if (okm::failed(r)) { // Clause 7.7: enquiry about a name that does not exist is answered, not // refused. A component of the name that is not a directory is the same - // answer, because the name still refers to nothing. - if (r == -okm::e_noent || r == -okm::e_notdir) { - *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; + // answer, and so is a node whose content names something absent when + // the enquiry resolves. + if (r == -okm::e_noent || r == -okm::e_notdir || r == -okm::e_loop) { + fill_absent(out); return kal_ok; } return okm::translate(r); } - fill_info(st, out); + fill_info(st, wanted, out); return kal_ok; } -int kal_fs_file_info(kal_file f, kal_node_info* out) { +int kal_fs_file_info(kal_file f, kal_u32 wanted, kal_node_info* out) { const int fd = okm::unpack(f.h); - if (fd < 0 || out == nullptr) return kal_err_invalid; + if (fd < 0 || !info_ok(out)) return kal_err_invalid; okm::kstat64 st{}; const okm_long r = okm::sys(okm::nr_fstat64, fd, reinterpret_cast(&st)); if (okm::failed(r)) return okm::translate(r); - fill_info(st, out); + fill_info(st, wanted, out); return kal_ok; } @@ -288,8 +325,9 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { return kal_ok; } -int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, - kal_uintptr* len, int* kind) { +int kal_fs_list_next(kal_dir, kal_uintptr* iter, + char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, int* kind) { if (iter == nullptr || *iter == 0) return kal_err_invalid; auto* s = reinterpret_cast(*iter); for (;;) { @@ -303,8 +341,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, okm::sys(okm::nr_close, s->fd); kal_free(s, sizeof(listing), alignof(listing)); *iter = 0; - if (name) *name = nullptr; - if (len) *len = 0; + if (name_len) *name_len = 0; return okm::failed(r) ? okm::translate(r) : kal_ok; } s->used = static_cast(r); @@ -316,8 +353,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, // They exist to support ascent, which this interface does not offer. if (e->name[0] == '.' && (e->name[1] == '\0' || (e->name[1] == '.' && e->name[2] == '\0'))) continue; - if (name) *name = e->name; - if (len) *len = e->namlen; + put_name(e->name, e->namlen, name_out, name_cap, name_len); if (kind) *kind = e->type == okm::dt_dir ? kal_node_directory : e->type == okm::dt_reg ? kal_node_file : e->type == okm::dt_lnk ? kal_node_link : kal_node_other; @@ -329,7 +365,96 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, // it is ordinarily installed on. A program that creates two names differing // only in case succeeds on the Linux implementation and not on this one, and // the position reports it in advance, which no operation could. -const kal_uintptr kal_fs_props = - KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; +// The properties of the volume a directory is on. +// +// AN ENQUIRY TAKING THE RESOURCE, BECAUSE EVERY POSITION IS A PROPERTY OF THE +// FORMAT. A word per implementation could state none of them 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 this +// implementation offers the whole filesystem as a preopen, so both are +// reachable through it. +// +// This kernel names the format in words rather than by a number, so that is +// what is consulted. For a format it does not recognise, what is claimed is the +// set that cannot be wrong: a modification time is reported for every volume it +// mounts, and `renameat' within one directory is atomic by POSIX. +kal_uintptr kal_fs_props(kal_dir d) { + const int fd = okm::unpack(d.h); + const kal_uintptr conservative = + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + if (fd < 0) return 0; + + okm::kstatfs64 sf{}; + const okm_long r = okm::sys(okm::nr_fstatfs64, fd, reinterpret_cast(&sf)); + if (okm::failed(r)) return conservative; + + const auto named = [&](const char* w) { + for (int i = 0; i < 16; ++i) { + if (sf.f_fstypename[i] != w[i]) return false; + if (w[i] == '\0') return true; + } + return false; + }; + + // Nodes whose content is another name, without a case distinction. This is + // the ordinary volume of this system. + if (named("apfs") || named("hfs") || named("autofs")) + return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + + // A case-sensitive volume with such nodes: a disk image formatted that way, + // or a network volume presenting one. + if (named("nfs") || named("smbfs") || named("webdav")) + return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + + // The FAT family stores neither a case distinction nor a node that names + // another. `symlinkat' on such a volume reports a refusal, and this is + // where a caller learns it before it tries. + if (named("msdos") || named("exfat")) return conservative; + + return conservative; +} + +// Nodes whose content is another name. +int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, + const char* target, kal_uintptr target_len, + kal_uintptr flags) { + // The target is content rather than a name this interface resolves, so it + // is not passed through `acceptable' --- which would refuse one that + // ascends, and one that ascends is the ordinary case for a relative target. + (void)flags; // this kernel does not distinguish a link to a directory + const int b = okm::unpack(base.h); + if (b < 0 || !okm::acceptable(name, len) || target == nullptr) return kal_err_invalid; + okm::terminated n(name, len); if (!n.ok) return kal_err_invalid; + okm::terminated tgt(target, target_len); if (!tgt.ok) return kal_err_invalid; + const okm_long r = okm::sys(okm::nr_symlinkat, reinterpret_cast(tgt.buf), + b, reinterpret_cast(n.buf)); + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + +kal_intptr kal_fs_link_read(kal_dir base, const char* name, kal_uintptr len, + char* out, kal_uintptr cap) { + const int b = okm::unpack(base.h); + if (b < 0 || !okm::acceptable(name, len)) return -kal_err_invalid; + okm::terminated t(name, len); if (!t.ok) return -kal_err_invalid; + + // The kernel truncates into the buffer it is given and does not report the + // length the content has, so a caller asking for the length --- a capacity + // of zero --- is served from a buffer of this implementation's own. + char own[okm::max_name + 1]; + char* dst = (out != nullptr && cap != 0) ? out : own; + okm_uptr room = (out != nullptr && cap != 0) ? cap : sizeof own; + okm_long r = okm::sys(okm::nr_readlinkat, b, reinterpret_cast(t.buf), + reinterpret_cast(dst), static_cast(room)); + if (okm::failed(r)) return -okm::translate(r); + + if (static_cast(r) == room && room < sizeof own) { + const okm_long full = okm::sys(okm::nr_readlinkat, b, + reinterpret_cast(t.buf), + reinterpret_cast(own), + static_cast(sizeof own)); + if (!okm::failed(full)) r = full; + } + return static_cast(r); +} } diff --git a/src/memory.cpp b/src/memory.cpp index 38a3adb..b9a20d0 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -21,7 +21,41 @@ namespace { -constexpr okm_uptr kPage = 4096; +// ⚠️⚠️ THE PAGE IS ASKED FOR, NOT ASSUMED. It was the constant 4096, and this +// system's own hardware has two page sizes: four kilobytes on one architecture +// and SIXTEEN on the other. A mapping rounded to four kilobytes on a machine +// whose page is sixteen is rounded up again by the kernel --- so allocation +// appeared to work --- while `kal_free' unmapped a range SHORTER than the one +// that was mapped, and the remainder was never returned. +// +// It is asked once. The value cannot change while the program runs, and the +// enquiry is a system call this allocator is otherwise not on the path of. +okm_uptr page_size() { + static okm_uptr cached = 0; + if (cached != 0) return cached; + + // hw.pagesize, by the numeric name the kernel takes: { CTL_HW, HW_PAGESIZE }. + int mib[2] = { 6, 7 }; + okm_u32 value = 0; + okm_uptr length = sizeof value; + const okm_long r = okm::sys(okm::nr_sysctl, + reinterpret_cast(mib), 2, + reinterpret_cast(&value), + reinterpret_cast(&length), 0, 0); + // The fallback is the architecture's own page rather than a number that is + // right on one of the two: a value smaller than the truth is what the + // defect above was made of. + if (okm::failed(r) || value == 0) { +#if defined(__aarch64__) + cached = 16384; +#else + cached = 4096; +#endif + } else { + cached = static_cast(value); + } + return cached; +} constexpr okm_uptr kMinBlock = 16; constexpr okm_uptr kMaxSmall = 32768; constexpr okm_uptr kChunk = 1u << 20; @@ -98,8 +132,9 @@ void* kal_alloc(kal_uintptr size, kal_uintptr align) { return b; } - const okm_uptr bytes = round_up(size, kPage); - if (align <= kPage) return map(bytes); + const okm_uptr page = page_size(); + const okm_uptr bytes = round_up(size, page); + if (align <= page) return map(bytes); // An alignment wider than a page is satisfied by mapping more and // recording, immediately before the region returned, what must be @@ -135,7 +170,9 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { return; } - if (align <= kPage) { unmap(p, round_up(size, kPage)); return; } + if (const okm_uptr page = page_size(); align <= page) { + unmap(p, round_up(size, page)); return; + } auto* user = static_cast(p); const okm_uptr total = reinterpret_cast(user)[-1]; @@ -143,4 +180,12 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { unmap(base, total); } + +// The quantum this environment allocates and protects memory in. This kernel +// allocates and protects in the same unit, so the coarsest of the two is that +// unit; a caller that rounds to it is never wrong. +kal_uintptr kal_memory_granularity(void) { + return static_cast(page_size()); +} + } diff --git a/src/net.cpp b/src/net.cpp index c1ed06f..4a3a619 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -140,12 +140,12 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { } } -kal_uintptr kal_net_stream(kal_net_conn c) { +kal_stream 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); + return kal_stream{ fd < 0 ? 0u : static_cast(fd) }; } int kal_net_peer(kal_net_conn c, kal_endpoint* out) { @@ -197,6 +197,6 @@ void kal_net_close_listener(kal_net_listener l) { // 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; +kal_uintptr kal_net_props(void) { return KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; } } // extern "C" diff --git a/src/process.cpp b/src/process.cpp index 54716a9..fac60c2 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -81,9 +81,9 @@ int kal_process_spawn(kal_dir base, if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; - const okm_long in = streams ? static_cast(streams->in) : 0; - const okm_long ou = streams ? static_cast(streams->out) : 0; - const okm_long er = streams ? static_cast(streams->err) : 0; + const okm_long in = streams ? static_cast(streams->in.h) : 0; + const okm_long ou = streams ? static_cast(streams->out.h) : 0; + const okm_long er = streams ? static_cast(streams->err.h) : 0; bool is_duplicate = false; const okm_long child = okm::duplicate(is_duplicate); @@ -182,9 +182,9 @@ int kal_process_spawn_with(kal_dir base, if (granted[i] < 0) return kal_err_invalid; } - const okm_long in = streams ? static_cast(streams->in) : 0; - const okm_long ou = streams ? static_cast(streams->out) : 0; - const okm_long er = streams ? static_cast(streams->err) : 0; + const okm_long in = streams ? static_cast(streams->in.h) : 0; + const okm_long ou = streams ? static_cast(streams->out.h) : 0; + const okm_long er = streams ? static_cast(streams->err.h) : 0; bool is_duplicate = false; const okm_long child = okm::duplicate(is_duplicate); @@ -247,9 +247,9 @@ int kal_process_terminate(kal_process h) { // waited for continues, and this environment collects it when the caller exits. void kal_process_close(kal_process) { } -const kal_uintptr kal_process_props = +kal_uintptr kal_process_props(void) { return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS - | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; + | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; } } diff --git a/src/random.cpp b/src/random.cpp index 2532d74..844568f 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -42,4 +42,4 @@ extern "C" int kal_random_fill(void* out, kal_uintptr len) { // Neither blocking nor hardware. This kernel's generator is seeded before a // process runs, so there is no wait to report; and whether the seed came from a // hardware source is not something this backend can observe. -extern "C" const kal_uintptr kal_random_props = 0; +extern "C" kal_uintptr kal_random_props(void) { return 0; } diff --git a/src/space.cpp b/src/space.cpp index 1204033..99a3f19 100644 --- a/src/space.cpp +++ b/src/space.cpp @@ -62,7 +62,7 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, // 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; +kal_uintptr kal_space_props(void) { return + KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; } } // extern "C" diff --git a/src/stream.cpp b/src/stream.cpp index 8f3bd9b..3337de2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -7,7 +7,8 @@ kal_stream kal_stdin (void) { return kal_stream{0}; } kal_stream kal_stdout(void) { return kal_stream{1}; } kal_stream kal_stderr(void) { return kal_stream{2}; } -kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { +// ONE SIGNED WORD: the count, or the negated condition when no byte moved. +kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { const auto* p = static_cast(buf); kal_uintptr done = 0; while (done < len) { @@ -20,23 +21,26 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { // reports it produces short writes on any system that delivers // signals --- a failure a test suite is unlikely to reproduce. if (okm::interrupted(r)) continue; - if (okm::failed(r)) return { done, okm::translate(r) }; + if (okm::failed(r)) { + if (done != 0) return static_cast(done); + return -okm::translate(r); + } if (r == 0) break; done += static_cast(r); } - return { done, done == len ? kal_ok : kal_err_io }; + return static_cast(done); } -kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { +kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { for (;;) { const okm_long r = okm::sys(okm::nr_read, static_cast(s.h), reinterpret_cast(buf), static_cast(len)); if (okm::interrupted(r)) continue; - if (okm::failed(r)) return { 0, okm::translate(r) }; + if (okm::failed(r)) return -okm::translate(r); // A short read is reported as it occurred. Unlike a short write it // carries information the caller requires: zero denotes end of input. - return { static_cast(r), kal_ok }; + return static_cast(r); } } diff --git a/src/sys.h b/src/sys.h index e248d4a..9a70117 100644 --- a/src/sys.h +++ b/src/sys.h @@ -179,6 +179,7 @@ enum : okm_long { nr_thread_selfid = 372, nr_openat = 463, nr_renameat = 465, nr_faccessat = 466, nr_fstatat64 = 470, nr_unlinkat = 472, nr_readlinkat = 473, + nr_symlinkat = 474, nr_fstatfs64 = 346, nr_sysctl = 202, nr_mkdirat = 475, nr_ulock_wait = 515, nr_ulock_wake = 516, @@ -302,6 +303,27 @@ inline int translate(okm_long r) { // --- this kernel's structure layouts ----------------------------------------- +// What this kernel reports about the volume a descriptor is on. It names the +// format in words rather than by a number, which is what this implementation +// consults --- a property that varies between the RESOURCES of an interface is +// answered by an enquiry taking the resource, and here the resource's format is +// what the enquiry has to look at. +struct kstatfs64 { + okm_u32 f_bsize; + okm_u32 f_iosize; // int32; the pair fills the first eight bytes + okm_u64 f_blocks, f_bfree, f_bavail, f_files, f_ffree; + okm_u32 f_fsid[2]; + okm_u32 f_owner; + okm_u32 f_type; + okm_u32 f_flags; + okm_u32 f_fssubtype; + char f_fstypename[16]; + char f_mntonname[1024]; + char f_mntfromname[1024]; + okm_u32 f_flags_ext; + okm_u32 f_reserved[7]; +}; + struct kstat64 { okm_u32 dev; okm_u32 mode_pad; // st_mode is 16 bits followed by 16 of nlink @@ -423,8 +445,14 @@ inline bool acceptable(const char* name, okm_uptr len) { return true; } +// The greatest length of a name this implementation accepts, which is the +// buffer below less the terminator it adds. A bound a caller cannot learn +// produces a failure it cannot attribute: a longer name was refused as +// kal_err_invalid, which is also the answer for a name that ascends. +inline constexpr okm_uptr max_name = 1023; + struct terminated { - char buf[1024]; + char buf[max_name + 1]; bool ok; terminated(const char* s, okm_uptr n) : ok(n < sizeof buf) { if (ok) { copy(buf, s, n); buf[n] = '\0'; } diff --git a/src/task.cpp b/src/task.cpp index 6696e38..b090b33 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -188,8 +188,8 @@ int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { // that compiled the program: this system's thread library establishes it for // every context it creates, which is why the position can be reported without // this implementation doing anything to earn it. -const kal_uintptr kal_task_props = +kal_uintptr kal_task_props(void) { return KAL_TASK_PROP_PREEMPTIVE | KAL_TASK_PROP_PARALLEL - | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; + | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; } } diff --git a/src/time.cpp b/src/time.cpp index f523edd..7649b18 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -79,7 +79,7 @@ void kal_time_sleep(kal_duration ns) { // exactly this divergence: a program measuring an interval across a suspension // obtains different answers from the two, and no operation could report which // it is dealing with. -const kal_uintptr kal_time_props = - KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; +kal_uintptr kal_time_props(void) { return + KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; } } diff --git a/src/timeout.cpp b/src/timeout.cpp index fec8b7b..439f2ef 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -63,10 +63,10 @@ int await(int fd, short events, kal_u64 ns) { extern "C" { -kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { +kal_intptr 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 }; + if (len == 0) return 0; const int fd = okm::unpack(s.h); // THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them @@ -75,18 +75,18 @@ kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 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 -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 }; +kal_intptr kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return 0; 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 -rc; return kal_stream_write(s, buf, len); } @@ -100,13 +100,13 @@ int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { 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) { +kal_intptr 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 (fd < 0) return -kal_err_invalid; if (const int rc = await(fd, static_cast(okm::poll_in), ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_datagram_recv_from(d, buf, len, from); } @@ -160,6 +160,6 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin // 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; +kal_u64 kal_timeout_granularity(void) { return 1000000u; } } // extern "C" diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..519a4f4 --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,21 @@ +#include "sys.h" +#include + +// What this implementation says about itself before it is used. Both answers are +// constants; openkal/version.h states why they belong to no interface. +extern "C" { + +kal_u64 kal_version(void) { return KAL_VERSION; } + +kal_u64 kal_interfaces(void) { + // Written out rather than derived: a word derived from what happens to be + // linked would report a facility as present when the linker had merely + // kept it. + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY + | KAL_IFACE_ENV | KAL_IFACE_TIME | KAL_IFACE_RANDOM + | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK + | KAL_IFACE_EXEC | KAL_IFACE_TERMINAL | KAL_IFACE_NET + | KAL_IFACE_DATAGRAM | KAL_IFACE_SPACE | KAL_IFACE_TIMEOUT; +} + +} From 0185ec1fda1e9da5f84d5b5870bac0a424af6678 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:39:16 +0800 Subject: [PATCH 2/4] 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. --- README.md | 4 +-- tests/conformance_env_time.cpp | 15 ++++++----- tests/conformance_fs.cpp | 40 +++++++++++++++++------------- tests/conformance_process_task.cpp | 10 +++++--- tests/conformance_stream.cpp | 13 ++++++---- 5 files changed, 48 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7929863..e209a2c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/tests/conformance_env_time.cpp b/tests/conformance_env_time.cpp index 68ed75f..fc56909 100644 --- a/tests/conformance_env_time.cpp +++ b/tests/conformance_env_time.cpp @@ -20,16 +20,19 @@ int main() { // A program always receives the name it was started with, even where the // environment has none, in which case it is empty rather than absent. check(kal::env::arg_count() >= 1, "at least the program name is present"); - kal_uintptr n = 0; - check(kal::env::arg(0, &n) != nullptr, "argument zero is readable"); + // ⭐ THE VALUE IS COPIED AND THE LENGTH REPORTED IS THE VALUE'S OWN, so a + // capacity of zero asks for the length without writing. + char buf[1024]; + check(kal::env::arg(0, buf, sizeof buf) >= 0, "argument zero is readable"); + check(kal::env::arg(0, nullptr, 0) == kal::env::arg(0, buf, sizeof buf), + "a capacity of zero reports the same length as a copy"); // A variable that is certain to exist under the harness, and one that is // certain not to. Both halves are asserted, because a lookup that always // succeeded and one that always failed would each satisfy only one. - kal_uintptr vlen = 0; - const char* path = kal::env::var("PATH", 4, &vlen); - check(path != nullptr && vlen > 0, "an existing variable is found"); - check(kal::env::var("OPENKAL_ABSENT_VARIABLE", 23, &vlen) == nullptr, + check(kal::env::var("PATH", 4, buf, sizeof buf) > 0, "an existing variable is found"); + check(kal::env::var("OPENKAL_ABSENT_VARIABLE", 23, buf, sizeof buf) + == -kal_err_not_found, "an absent variable is reported absent"); check(kal_env_var_count() > 0, "the set can be enumerated"); diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index 4255d02..f490e0d 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -25,38 +25,41 @@ int main() { check(root.h != 0, "the working directory is the first entry"); // Each supplied directory carries the name the environment gives it. - kal_dir d0{}; const char* n0 = nullptr; kal_uintptr l0 = 0; - check(kal_fs_preopen(0, &d0, &n0, &l0) == kal_ok && n0 != nullptr && l0 > 0, + kal_dir d0{}; char n0[512]; kal_uintptr l0 = 0; + check(kal_fs_preopen(0, &d0, n0, sizeof n0, &l0) == kal_ok && l0 > 0, "a supplied directory carries a name"); kal_dir beyond{}; - check(kal_fs_preopen(kal::fs::preopen_count(), &beyond, nullptr, nullptr) != kal_ok, + check(kal_fs_preopen(kal::fs::preopen_count(), &beyond, nullptr, 0, nullptr) != kal_ok, "an index beyond the set is refused"); // Creation, writing, reading back, and removal. kal_file f{}; - check(kal_fs_open_file(root, "okl_probe.txt", 13, 1, 1, &f) == kal_ok, + check(kal_fs_open(root, "okl_probe.txt", 13, + (kal::fs::open::read | kal::fs::open::write + | kal::fs::open::create | kal::fs::open::truncate).bits, &f) == kal_ok, "a file is created"); - const kal_stream s{ kal_fs_stream(f) }; + const kal_stream s = kal_fs_stream(f); const char payload[] = "conformance"; - check(kal::write(s, payload, sizeof(payload) - 1).e == kal_ok, "the file is written"); + check(kal::write(s, payload, sizeof(payload) - 1) + == static_cast(sizeof(payload) - 1), "the file is written"); __UINT64_TYPE__ pos = 0; check(kal_fs_seek(f, 0, kal::fs::seek_set, &pos) == kal_ok && pos == 0, "the file is repositioned"); char back[32] = {}; - const auto r = kal::read(s, back, sizeof(back)); - check(r.e == kal_ok && r.n == sizeof(payload) - 1, "the file reads back"); + const kal_intptr r = kal::read(s, back, sizeof(back)); + check(r == static_cast(sizeof(payload) - 1), "the file reads back"); for (kal_uintptr i = 0; i < sizeof(payload) - 1; ++i) check(back[i] == payload[i], "the contents match"); kal_fs_close_file(f); // Enquiry reports what was written, and reports absence without failing. - kal_node_info info{}; - check(kal_fs_info(root, "okl_probe.txt", 13, &info) == kal_ok, "enquiry succeeds"); + kal_node_info info{}; info.self_size = sizeof info; + check(kal_fs_info(root, "okl_probe.txt", 13, 0, kal::fs::field::all, &info) == kal_ok, "enquiry succeeds"); check(info.kind == kal_node_file, "the node is a file"); check(info.size == sizeof(payload) - 1, "the size is reported"); - kal_node_info absent{}; - check(kal_fs_info(root, "okl_absent", 10, &absent) == kal_ok + kal_node_info absent{}; absent.self_size = sizeof absent; + check(kal_fs_info(root, "okl_absent", 10, 0, kal::fs::field::all, &absent) == kal_ok && absent.kind == kal_node_absent, "an absent name is reported absent rather than as a failure"); @@ -65,14 +68,17 @@ int main() { kal_dir d{}; check(kal_fs_open_dir(root, "okl_dir", 7, &d) == kal_ok, "the directory opens"); kal_file inner{}; - check(kal_fs_open_file(d, "inner", 5, 1, 1, &inner) == kal_ok, "a file is created within"); + check(kal_fs_open(d, "inner", 5, + (kal::fs::open::read | kal::fs::open::write + | kal::fs::open::create | kal::fs::open::truncate).bits, &inner) == kal_ok, "a file is created within"); kal_fs_close_file(inner); kal_uintptr iter = 0; bool found = false; check(kal_fs_list_begin(d, &iter) == kal_ok, "enumeration begins"); for (;;) { - const char* name = nullptr; kal_uintptr len = 0; int kind = 0; - if (kal_fs_list_next(d, &iter, &name, &len, &kind) != kal_ok) break; - if (name == nullptr) break; + char name[512]; kal_uintptr len = 0; int kind = 0; + if (kal_fs_list_next(d, &iter, name, sizeof name, &len, &kind) != kal_ok) break; + // The iterator becoming zero is how the end is reported now. + if (iter == 0) break; if (len == 5 && name[0] == 'i') found = true; } check(found, "enumeration finds the entry"); @@ -87,7 +93,7 @@ int main() { // A released handle is not valid, which the specification requires. kal_fs_close_dir(d); kal_file after{}; - check(kal_fs_open_file(d, "inner", 5, 0, 0, &after) != kal_ok, + check(kal_fs_open(d, "inner", 5, kal::fs::open::read.bits, &after) != kal_ok, "a released handle is not treated as valid"); check(kal_fs_remove(root, "okl_dir/inner", 13) == kal_ok, "the inner file is removed"); diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index 0bbaca8..5353253 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -37,12 +37,14 @@ int main() { // The program to start is reached through a directory the environment // supplied, which is the whole reason the set exists: a program and the // program it starts are commonly not beneath one root. - kal_dir slash{}; const char* nm = nullptr; kal_uintptr nl = 0; + kal_dir slash{}; char nm[512] = {}; kal_uintptr nl = 0; bool have_root = false; for (kal_uintptr i = 0; i < kal::fs::preopen_count(); ++i) { - kal_dir d{}; const char* n = nullptr; kal_uintptr l = 0; - if (kal_fs_preopen(i, &d, &n, &l) != kal_ok) continue; - if (l == 1 && n[0] == '/') { slash = d; nm = n; nl = l; have_root = true; } + kal_dir d{}; char n[512]; kal_uintptr l = 0; + if (kal_fs_preopen(i, &d, n, sizeof n, &l) != kal_ok) continue; + if (l == 1 && n[0] == '/') { + slash = d; nm[0] = '/'; nm[1] = '\0'; nl = l; have_root = true; + } } check(have_root, "a directory covering the file system is supplied"); diff --git a/tests/conformance_stream.cpp b/tests/conformance_stream.cpp index 307fbf7..85d9ed9 100644 --- a/tests/conformance_stream.cpp +++ b/tests/conformance_stream.cpp @@ -33,13 +33,16 @@ int main() { // specification excludes a successful partial transfer, so a conforming // result reports either the full count or a non-zero error. const char msg[] = "openkal-linux: conformance\n"; - const auto r = kal::write(kal::out(), msg, sizeof(msg) - 1); - check(r.e == kal_ok, "write reports success"); - check(r.n == sizeof(msg) - 1, "write transfers the whole buffer"); + // ⭐ ONE SIGNED WORD: the count, or the negated condition when no byte + // moved. A caller never inspects two things to learn one thing. + const kal_intptr r = kal::write(kal::out(), msg, sizeof(msg) - 1); + check(r >= 0, "write reports success"); + check(r == static_cast(sizeof(msg) - 1), + "write transfers the whole buffer"); // An invalid handle is reported rather than accepted. - const auto bad = kal::write(kal::stream{ 0x7fffffff }, msg, 1); - check(bad.e != kal_ok, "an invalid handle is rejected"); + const kal_intptr bad = kal::write(kal::stream{ 0x7fffffff }, msg, 1); + check(bad < 0, "an invalid handle is rejected"); // Flushing an unbuffered stream succeeds. check(kal::flush(kal::out()) == kal_ok, "flush succeeds"); From e8587739a2cf5138310817c889a95ad2d5f5538e Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:12:28 +0800 Subject: [PATCH 3/4] 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. --- src/exec.cpp | 101 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/src/exec.cpp b/src/exec.cpp index e14061c..9cdb886 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -1,5 +1,6 @@ #include "sys.h" #include +#include // openkal.exec on this system. // @@ -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(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(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(bytes), + okm::prot_read | okm::prot_exec); + okm::sys(okm::nr_munmap, m, static_cast(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(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(size), granularity()); 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); @@ -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(size), kPage); + const okm_uptr bytes = round_up(static_cast(size), granularity()); const okm_long r = okm::sys(okm::nr_mprotect, reinterpret_cast(p), static_cast(bytes), okm::prot_read | okm::prot_exec); @@ -109,22 +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(size), kPage); + const okm_uptr bytes = round_up(static_cast(size), granularity()); 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. -// Executable memory on this system is granted only to an artifact produced -// with the entitlement for it, which is a decision made after the link. The -// interface is provided and the position reports whether this artifact may use -// it --- clause 6.5's answer at dependency resolution cannot serve an artifact -// produced once and run in many environments. -kal_uintptr kal_exec_props(void) { return 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" From 3e17834b483a34c46a3a8f64676f0f3ae40180bf Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:16:41 +0800 Subject: [PATCH 4/4] 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. --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6206d1..7b9e062 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 + 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