From 1422a1437623fa69b4d2b3301de1dc1410d962a5 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 00:45:55 +0800 Subject: [PATCH 1/4] 0.7.0 --- adopt openkal 0.9, and answer per resource what was answered per implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every report becomes an operation, so an implementation reports what is true of the resource the caller named rather than one word for the whole machine. The change that has teeth is `kal_fs_props`: * it took no argument and claimed KAL_FS_PROP_CASE_SENSITIVE unconditionally. This implementation offers the whole filesystem as a preopen, so a FAT volume mounted anywhere on the machine is reachable through it, and the claim is false there. It now consults the format the resource is on; * it never claimed KAL_FS_PROP_LINKS while every operation here met them. ⚠️ AND ASKING NOW RESOLVES A LINK, BECAUSE OPENING ALWAYS DID. `kal_fs_info` passed AT_SYMLINK_NOFOLLOW always while `kal_fs_open` did not set O_NOFOLLOW --- the constant was declared in sys.h and used nowhere. So in one program, opening a name reached a file and asking about it reported a link. A C library above this reported `is_regular_file` as false for a name whose bytes it could read, and one such node made a whole tree uncopyable. The rest follows the specification: * transfers return one signed word; * `kal_env_*` and `kal_fs_preopen` and `kal_fs_list_next` copy into the caller's buffer and report the length the value has; * `kal_node_info` carries its own size, reports what was filled, and carries the device and inode as an opaque identity a caller may compare and not read --- which is what stops two different files reading as one; * `kal_fs_link_create` and `kal_fs_link_read` over symlinkat and readlinkat, with availability answered by the enquiry before they are called; * `kal_memory_granularity` from the auxiliary vector rather than a constant; * `kal_fs_max_name`, because 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; * `kal_fs_stream` and `kal_spawn_streams` carry their type; * `kal_version` and `kal_interfaces`, answered with constants. 168 conformance observations hold, up from 143. --- .gitignore | 7 +- mcpp.toml | 4 +- src/datagram.cpp | 24 ++--- src/env.cpp | 50 +++++++---- src/exec.cpp | 6 +- src/fs.cpp | 229 ++++++++++++++++++++++++++++++++++++++--------- src/memory.cpp | 24 +++++ src/net.cpp | 9 +- src/process.cpp | 21 ++--- src/random.cpp | 2 +- src/space.cpp | 5 +- src/stream.cpp | 19 ++-- src/sys.h | 56 +++++++++++- src/task.cpp | 7 +- src/time.cpp | 7 +- src/timeout.cpp | 22 ++--- src/version.cpp | 29 ++++++ 17 files changed, 402 insertions(+), 119 deletions(-) create mode 100644 src/version.cpp diff --git a/.gitignore b/.gitignore index 08ea216..f70e796 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,9 @@ compile_commands.json Thumbs.db # The specification tree tools/run-conformance.sh clones beside the sources. -.spec/ +# +# ⚠️ NO TRAILING SLASH. `.spec/' matches a directory and does not match a +# SYMBOLIC LINK to one, which is what a working checkout naturally has; the link +# was consequently committed once, pointing at a path that exists on one +# machine. +.spec diff --git a/mcpp.toml b/mcpp.toml index 2aebe52..1a3ab9d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.6.0" +version = "0.7.0" description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-linux" [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/src/datagram.cpp b/src/datagram.cpp index 1569f60..5a6de3f 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -71,15 +71,15 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) { return okl::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; okl::ksockaddr_storage ss{}; okl_long addrlen = 0; if (const int rc = okl::to_kernel(*to, ss, addrlen); rc != kal_ok) - return { 0, rc }; + return -rc; for (;;) { const okl_long r = okl::sys(okl::nr_sendto, fd, @@ -87,7 +87,7 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr static_cast(len), 0, reinterpret_cast(&ss), addrlen); if (okl::interrupted(r)) continue; - if (okl::failed(r)) return { 0, okl::translate(r) }; + if (okl::failed(r)) return -okl::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 @@ -96,14 +96,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; okl::ksockaddr_storage ss{}; okl_long addrlen = static_cast(sizeof ss); @@ -115,7 +115,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, reinterpret_cast(&ss), reinterpret_cast(&addrlen)); if (okl::interrupted(r)) continue; - if (okl::failed(r)) return { 0, okl::translate(r) }; + if (okl::failed(r)) return -okl::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 @@ -131,7 +131,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); } } @@ -146,6 +146,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 09a64c5..2cc61ef 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -41,48 +41,64 @@ namespace { extern "C" { +// 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 --- so the interface said +// something different depending on how it was reached, which is the one thing a +// contract must not do (clause 4.4). +// +// Each returns the length the value HAS, so a caller with a large enough buffer +// is done in one call, a caller that wants to size first passes a capacity of +// zero, and a caller whose buffer was too small learns it by comparing. +namespace { +kal_intptr give(const char* v, kal_uintptr n, char* out, kal_uintptr cap) { + if (out != nullptr && cap != 0) okl::copy(out, v, n < cap ? n : cap); + return static_cast(n); +} +} // namespace + kal_uintptr kal_env_arg_count(void) { return static_cast(okl::g_argc); } -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) { - if (index >= static_cast(okl::g_argc)) { if (len) *len = 0; return nullptr; } +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { + if (index >= static_cast(okl::g_argc)) return -kal_err_not_found; const char* s = okl::g_argv[index]; - if (len) *len = okl::length(s); - return s; + return give(s, okl::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 = okl::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 = okl::length(v); - return v; + return give(v, okl::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, and + // reporting a length of zero for both would lose that. + return -kal_err_not_found; } kal_uintptr kal_env_var_count(void) { kal_uintptr n = 0; for (char** e = okl::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. The set does not change +// while the program runs, so an index may be held across the two calls. +kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) { kal_uintptr n = 0; for (char** e = okl::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 = okl::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 5ebbb32..4fdec45 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -95,6 +95,10 @@ void kal_exec_free(void* p, kal_uintptr size) { // A published region may be reserved for writing again: this kernel's // protection call is not one-way. The position is set accordingly, and a // caller that must change published bytes need not abandon the region. -const kal_uintptr kal_exec_props = KAL_EXEC_PROP_REPUBLISH; +kal_uintptr kal_exec_props(void) { + // Executable memory is available to every artifact on this kernel: nothing + // here is granted only to a program produced in a particular way. + return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE; +} } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index e7bcd49..e252bc7 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -58,14 +58,58 @@ int kind_of(okl_u32 mode) { } } -void fill_info(const okl::kstat& 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(st.mode), - (st.mode & 0200u) != 0 ? 1 : 0, - }; +// Writes no more of the structure than the caller says exists on its side, and +// reports which fields it filled. +// +// EVERY FIELD IS FILLED AND `wanted' IS IGNORED, WHICH IS THE ONE LINE THE +// SPECIFICATION SAYS THIS SHOULD BE. One `newfstatat' answers all of them on +// this kernel, so selecting would cost a branch and save nothing. An +// implementation whose environment answers them separately is the one `wanted' +// exists for. +void fill_info(const okl::kstat& 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); + // The identity is the pair the kernel already keeps. It is opaque to a + // caller, which may compare it and may not read it; using the device and + // the inode is this kernel's answer and not the interface's shape. + v.identity[0] = st.dev; + v.identity[1] = st.ino; + v.kind = kind_of(st.mode); + v.writable = (st.mode & 0200u) != 0 ? 1 : 0; + + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); +} + +// A node that refers to nothing, in the same shape. +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; + okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); +} + +// The caller must state how much of the structure exists on its side. A +// consumer that did not is a consumer whose structure this cannot be. +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) okl::copy(out, src, n < cap ? n : cap); + if (len) *len = n; + return n; } // Enumeration reads the kernel's own directory records. A C library's @@ -87,14 +131,14 @@ 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; } @@ -130,17 +174,6 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, return kal_ok; } -// The form the earlier version specified, defined in terms of the one above, -// which is what the specification records that an implementation ordinarily -// does. -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 = okl::unpack(d.h); if (fd >= 0) { okl::retire(d.h); okl::sys(okl::nr_close, fd); } @@ -155,11 +188,14 @@ void kal_fs_close_file(kal_file f) { // holds on this implementation, so no conversion is required and none is // performed: the two interfaces agree because both are descriptor-shaped here, // which is a property of this implementation and not of the specification. -kal_uintptr kal_fs_stream(kal_file f) { +kal_stream kal_fs_stream(kal_file f) { const int fd = okl::unpack(f.h); - return fd < 0 ? 0u : static_cast(fd); + return kal_stream{ fd < 0 ? 0u : static_cast(fd) }; } +// The greatest length of a name this implementation accepts. +kal_uintptr kal_fs_max_name(void) { return okl::max_name; } + int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { const int fd = okl::unpack(f.h); if (fd < 0) return kal_err_invalid; @@ -179,34 +215,44 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okl::failed(r) ? okl::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 = okl::unpack(base.h); - if (b < 0 || out == nullptr || !okl::acceptable(name, len)) return kal_err_invalid; + if (b < 0 || !info_ok(out) || !okl::acceptable(name, len)) return kal_err_invalid; okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; okl::kstat 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. + // A C library above reported a link where the host reports a regular file, + // and one symbolic link made a whole tree uncopyable. + const okl_long at = (flags & KAL_FS_NO_RESOLVE) ? okl::at_symlink_nofollow : 0; const okl_long r = okl::sys(okl::nr_newfstatat, b, reinterpret_cast(t.buf), - reinterpret_cast(&st), okl::at_symlink_nofollow); + reinterpret_cast(&st), at); if (okl::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 == -okl::e_noent || r == -okl::e_notdir) { - *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; + // same answer, because the name still refers to nothing --- and so is a + // node whose content names something absent, when the enquiry resolves. + if (r == -okl::e_noent || r == -okl::e_notdir || r == -okl::e_loop) { + fill_absent(out); return kal_ok; } return okl::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 = okl::unpack(f.h); - if (fd < 0 || out == nullptr) return kal_err_invalid; + if (fd < 0 || !info_ok(out)) return kal_err_invalid; okl::kstat st{}; const okl_long r = okl::sys(okl::nr_fstat, fd, reinterpret_cast(&st)); if (okl::failed(r)) return okl::translate(r); - fill_info(st, out); + fill_info(st, wanted, out); return kal_ok; } @@ -282,8 +328,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 (;;) { @@ -296,8 +343,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, okl::sys(okl::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 okl::failed(r) ? okl::translate(r) : kal_ok; } s->used = static_cast(r); @@ -309,8 +355,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 = okl::length(e->name); + put_name(e->name, okl::length(e->name), name_out, name_cap, name_len); if (kind) *kind = e->type == okl::dt_dir ? kal_node_directory : e->type == okl::dt_reg ? kal_node_file : e->type == okl::dt_lnk ? kal_node_link : kal_node_other; @@ -318,8 +363,106 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, } } -const kal_uintptr kal_fs_props = - KAL_FS_PROP_CASE_SENSITIVE | 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. Version 0.6 answered with one word per implementation and claimed +// case sensitivity in it unconditionally --- which is false on any machine that +// has a FAT volume mounted, and this implementation offers the whole filesystem +// as a preopen, so such a volume is reachable through it. It also never claimed +// links, while every operation here met them. +// +// What is claimed for a format this implementation does not recognise is the +// set that cannot be wrong: the kernel reports a modification time for every +// filesystem it mounts, and `renameat' within one directory is atomic by POSIX. +// Case sensitivity and links are claimed only where the format is known to have +// them. +kal_uintptr kal_fs_props(kal_dir d) { + const int fd = okl::unpack(d.h); + const kal_uintptr conservative = + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + if (fd < 0) return 0; + + okl::kstatfs sf{}; + const okl_long r = okl::sys(okl::nr_fstatfs, fd, reinterpret_cast(&sf)); + if (okl::failed(r)) return conservative; + + switch (sf.f_type) { + // Formats with a case-sensitive namespace and nodes that name others. + case okl::fs_ext234: case okl::fs_btrfs: case okl::fs_xfs: + case okl::fs_f2fs: case okl::fs_tmpfs: case okl::fs_overlay: + case okl::fs_zfs: case okl::fs_bcachefs: + return conservative | KAL_FS_PROP_CASE_SENSITIVE + | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + + // Read-only formats: the nodes are there and none can be made, and a + // rename cannot be atomic because there is no rename. + case okl::fs_squashfs: case okl::fs_erofs: + return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE + | KAL_FS_PROP_LINKS; + case okl::fs_iso9660: + return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE; + + // The FAT family stores neither a case distinction nor a node that + // names another. `symlink' on such a volume reports EPERM, and this is + // where a caller learns that before it tries. + case okl::fs_msdos: case okl::fs_exfat: + return conservative; + + // A case-insensitive namespace, with nodes that name others. + case okl::fs_ntfs: case okl::fs_ntfs3: case okl::fs_hfsplus: + return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + + default: + 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 not a name this interface resolves: it is content, stored + // and interpreted by whoever follows it later. It is therefore not passed + // through `acceptable', which would refuse a target that ascends --- and a + // target that ascends is the ordinary case for a relative one. + (void)flags; // this kernel does not distinguish a link to a directory + const int b = okl::unpack(base.h); + if (b < 0 || !okl::acceptable(name, len) || target == nullptr) return kal_err_invalid; + okl::terminated n(name, len); if (!n.ok) return kal_err_invalid; + okl::terminated tgt(target, target_len); if (!tgt.ok) return kal_err_invalid; + const okl_long r = okl::sys(okl::nr_symlinkat, reinterpret_cast(tgt.buf), + b, reinterpret_cast(n.buf)); + return okl::failed(r) ? okl::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 = okl::unpack(base.h); + if (b < 0 || !okl::acceptable(name, len)) return -kal_err_invalid; + okl::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[okl::max_name + 1]; + char* dst = (out != nullptr && cap != 0) ? out : own; + okl_uptr room = (out != nullptr && cap != 0) ? cap : sizeof own; + okl_long r = okl::sys(okl::nr_readlinkat, b, reinterpret_cast(t.buf), + reinterpret_cast(dst), static_cast(room)); + if (okl::failed(r)) return -okl::translate(r); + + // A result equal to the room given may have been truncated. Asking again + // with room of this implementation's own is what turns "at least this" into + // "this", and it is the only way this kernel offers. + if (static_cast(r) == room && room < sizeof own) { + const okl_long full = okl::sys(okl::nr_readlinkat, b, + reinterpret_cast(t.buf), + reinterpret_cast(own), + static_cast(sizeof own)); + if (!okl::failed(full)) r = full; + } + return static_cast(r); +} } diff --git a/src/memory.cpp b/src/memory.cpp index f3eacad..5449d45 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -18,6 +18,8 @@ // library defines `malloc' too, so the call resolves to it, and it in turn // calls this implementation. See src/sys.h. +namespace okl { okl_ulong auxval(okl_ulong key); } + namespace { @@ -143,4 +145,26 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { unmap(base, total); } + +// The quantum this environment allocates and protects memory in. +// +// AN OPERATION AND NOT A CONSTANT, BECAUSE IT IS A PROPERTY OF THE MACHINE THE +// PROGRAM RUNS ON. A C library above this reports it as its own page size; one +// that fixed it when it was built is wrong on every machine whose quantum +// differs from the one it was built for, which is what a distributed binary +// meets --- sixteen kilobytes on one family of hardware, sixty-four on another. +// +// The kernel states it in the auxiliary vector it leaves at inception. Where +// that vector is absent --- a program whose entry did not record it --- the +// value falls back to the architecture's smallest page, which is the smallest +// quantum this kernel ever uses and is therefore never coarser than the truth. +// +// One number, and it is the coarsest that is always safe: this kernel allocates +// and protects in the same unit, so the two are the same value here. An +// implementation on a system where they differ reports the coarser. +kal_uintptr kal_memory_granularity(void) { + const okl_ulong page = okl::auxval(6 /* AT_PAGESZ */); + return page != 0 ? static_cast(page) : kPage; +} + } diff --git a/src/net.cpp b/src/net.cpp index aab1754..1f4e56d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -127,12 +127,13 @@ 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. + // packed word is not that. It carries its type, so a handle crossing + // between two interfaces is not a word either of them has to interpret. 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) { @@ -186,6 +187,6 @@ void kal_net_close_listener(kal_net_listener l) { // the socket call then reports it at the point of the attempt, and a word that // claimed less than the kernel offers would withhold a facility a caller could // have used. -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 d3e4c2b..43eb172 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -70,9 +70,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 okl_long in = streams ? static_cast(streams->in) : 0; - const okl_long ou = streams ? static_cast(streams->out) : 0; - const okl_long er = streams ? static_cast(streams->err) : 0; + const okl_long in = streams ? static_cast(streams->in.h) : 0; + const okl_long ou = streams ? static_cast(streams->out.h) : 0; + const okl_long er = streams ? static_cast(streams->err.h) : 0; // The image is duplicated and then replaced. openkal has no operation that // duplicates the calling image, and this is why: the duplicate is not a @@ -183,9 +183,9 @@ int kal_process_spawn_with(kal_dir base, if (granted[i] < 0) return kal_err_invalid; } - const okl_long in = streams ? static_cast(streams->in) : 0; - const okl_long ou = streams ? static_cast(streams->out) : 0; - const okl_long er = streams ? static_cast(streams->err) : 0; + const okl_long in = streams ? static_cast(streams->in.h) : 0; + const okl_long ou = streams ? static_cast(streams->out.h) : 0; + const okl_long er = streams ? static_cast(streams->err.h) : 0; const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0); if (okl::failed(child)) return okl::translate(child); @@ -251,9 +251,10 @@ 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_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING - | KAL_PROCESS_PROP_EXIT_STATUS - | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; +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; +} } diff --git a/src/random.cpp b/src/random.cpp index 70fc828..4d14f65 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -52,4 +52,4 @@ extern "C" int kal_random_fill(void* out, kal_uintptr len) { // Blocking, because GRND_NONBLOCK is not set above. Not hardware: the kernel's // pool is what this reads, and whether the pool was seeded from a hardware // source is not something this backend can observe. -extern "C" const kal_uintptr kal_random_props = KAL_RANDOM_PROP_BLOCKING; +extern "C" kal_uintptr kal_random_props(void) { return KAL_RANDOM_PROP_BLOCKING; } diff --git a/src/space.cpp b/src/space.cpp index 0b5bf5b..6c7bf27 100644 --- a/src/space.cpp +++ b/src/space.cpp @@ -61,7 +61,8 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, // memory can fail with the machine out of memory after this call has already // reported success. An implementation cannot undefer that, and stating it is // what lets a program that cannot tolerate it know which environment it is in. -const kal_uintptr kal_space_props = - KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; +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 3cd975c..95575ce 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -27,7 +27,11 @@ 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. +// Clause 5.2.1. A caller that moved some bytes and then met a condition is told +// how many moved and meets the condition on its next call, which is what every +// consumer of the earlier two-word form did with it by hand. +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) { @@ -40,23 +44,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 (okl::interrupted(r)) continue; - if (okl::failed(r)) return { done, okl::translate(r) }; + if (okl::failed(r)) { + if (done != 0) return static_cast(done); + return -okl::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 okl_long r = okl::sys(okl::nr_read, static_cast(s.h), reinterpret_cast(buf), static_cast(len)); if (okl::interrupted(r)) continue; - if (okl::failed(r)) return { 0, okl::translate(r) }; + if (okl::failed(r)) return -okl::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 1ab9e0a..980dee6 100644 --- a/src/sys.h +++ b/src/sys.h @@ -96,7 +96,7 @@ enum : okl_long { nr_clock_getres = 229, nr_exit_group = 231, nr_tgkill = 234, nr_openat = 257, nr_mkdirat = 258, nr_newfstatat = 262, nr_unlinkat = 263, nr_renameat = 264, nr_readlinkat = 267, nr_dup3 = 292, nr_execveat = 322, - nr_dup2 = 33, nr_utimensat = 280, + nr_dup2 = 33, nr_utimensat = 280, nr_symlinkat = 266, nr_fstatfs = 138, nr_getrandom = 318, // openkal.net and openkal.datagram nr_socket = 41, nr_connect = 42, nr_accept = 43, nr_sendto = 44, @@ -179,7 +179,7 @@ enum : okl_long { nr_getpid = 172, nr_mmap = 222, nr_munmap = 215, nr_mprotect = 226, nr_clone = 220, nr_execve = 221, nr_wait4 = 260, nr_renameat = 38, nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1, - nr_arch_prctl = -1, nr_utimensat = 88, + nr_arch_prctl = -1, nr_utimensat = 88, nr_symlinkat = 36, nr_fstatfs = 44, nr_getrandom = 278, // openkal.net and openkal.datagram nr_socket = 198, nr_connect = 203, nr_accept = 202, nr_sendto = 206, @@ -259,6 +259,47 @@ inline bool interrupted(okl_long r) { return r == -e_intr; } // --- the kernel's structure layouts ---------------------------------------- +// What the kernel reports about the volume a descriptor is on. The layout is +// the kernel's own `struct statfs', which is one layout on every architecture +// this implementation supports because both are LP64. +struct kstatfs { + okl_long f_type; + okl_long f_bsize; + okl_u64 f_blocks; + okl_u64 f_bfree; + okl_u64 f_bavail; + okl_u64 f_files; + okl_u64 f_ffree; + okl_u64 f_fsid; + okl_long f_namelen; + okl_long f_frsize; + okl_long f_flags; + okl_long f_spare[4]; +}; + +// The magic numbers the kernel reports in `f_type', from its own uapi header. +// A property that varies between the RESOURCES of an interface is answered by +// an enquiry taking the resource, and on this kernel the resource's format is +// what the enquiry has to consult. +enum : okl_long { + fs_ext234 = 0xEF53, + fs_btrfs = 0x9123683E, + fs_xfs = 0x58465342, + fs_f2fs = 0xF2F52010, + fs_tmpfs = 0x01021994, + fs_overlay = 0x794C7630, + fs_zfs = 0x2FC12FC1, + fs_bcachefs = 0xCA451A4E, + fs_msdos = 0x4D44, // vfat, and every FAT before it + fs_exfat = 0x2011BAB0, + fs_ntfs = 0x5346544E, + fs_ntfs3 = 0x7366746E, + fs_iso9660 = 0x9660, + fs_squashfs = 0x73717368, + fs_erofs = 0xE0F5E1E2, + fs_hfsplus = 0x482B, +}; + struct kstat { okl_u64 dev; okl_u64 ino; @@ -433,10 +474,19 @@ inline bool acceptable(const char* name, okl_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 THE CALLER CANNOT ATTRIBUTE. +// A longer name was refused as kal_err_invalid, which is also the answer for a +// name that ascends --- so a program meeting the bound was told that its name +// was malformed. `kal_fs_max_name' reports this. +inline constexpr okl_uptr max_name = 4095; + // A counted name becomes a terminated one for the kernel. The conversion is a // change of representation, not a namespace being reconstructed. struct terminated { - char buf[4096]; + char buf[max_name + 1]; bool ok; terminated(const char* s, okl_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 fb3e820..753ca54 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -260,8 +260,9 @@ int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { // The thread-local position is reported in both configurations, and it is true // in both for different reasons: the C library's threads establish the // convention, and so does the block this implementation builds. Clause 7.10. -const kal_uintptr kal_task_props = - KAL_TASK_PROP_PREEMPTIVE | KAL_TASK_PROP_PARALLEL - | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; +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; +} } diff --git a/src/time.cpp b/src/time.cpp index fd224f3..9135f9d 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -46,8 +46,9 @@ void kal_time_sleep(kal_duration ns) { // The monotonic clock of this kernel does not advance while the machine is // suspended, which the corresponding property records. -const kal_uintptr kal_time_props = - KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_MONOTONIC_SUSPENDS - | KAL_TIME_PROP_SLEEP_PRECISE; +kal_uintptr kal_time_props(void) { + return KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_MONOTONIC_SUSPENDS + | KAL_TIME_PROP_SLEEP_PRECISE; +} } diff --git a/src/timeout.cpp b/src/timeout.cpp index a6f336d..c557c57 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -56,10 +56,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 = okl::unpack(s.h); // THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them @@ -67,17 +67,17 @@ kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 // be one of those rather than being refused. const int use = (fd >= 0) ? fd : static_cast(s.h); - if (const int rc = await(use, okl::poll_in, ns); rc != kal_ok) return { 0, rc }; + if (const int rc = await(use, okl::poll_in, ns); rc != kal_ok) 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 = okl::unpack(s.h); const int use = (fd >= 0) ? fd : static_cast(s.h); - if (const int rc = await(use, okl::poll_out, ns); rc != kal_ok) return { 0, rc }; + if (const int rc = await(use, okl::poll_out, ns); rc != kal_ok) return -rc; return kal_stream_write(s, buf, len); } @@ -90,12 +90,12 @@ 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 = okl::unpack(d.h); - if (fd < 0) return { 0, kal_err_invalid }; + if (fd < 0) return -kal_err_invalid; - if (const int rc = await(fd, okl::poll_in, ns); rc != kal_ok) return { 0, rc }; + if (const int rc = await(fd, okl::poll_in, ns); rc != kal_ok) return -rc; return kal_datagram_recv_from(d, buf, len, from); } @@ -148,6 +148,6 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin // interval the child-waiting loop above polls at, and a caller asking for less // than the coarsest of the operations here would otherwise be told a number one // of them 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..9102e34 --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,29 @@ +#include "sys.h" +#include + +// What this implementation says about itself before it is used. +// +// NOT AN INTERFACE, AND SO NOT CONDITIONAL ON ONE. Every conforming +// implementation exports both names; clause 3.2's closure is of the set of core +// INTERFACES, and these provide no resource. A consumer that is linked learns an +// interface's absence from the linker, and one bound at load or across a +// boundary has no linker to ask --- so it asks here, before it calls anything. +// +// Both are constants on this implementation, which is what the specification +// says the cheap answer should be. +extern "C" { + +kal_u64 kal_version(void) { return KAL_VERSION; } + +kal_u64 kal_interfaces(void) { + // Every interface this implementation provides. It is written out rather + // than derived, because 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 0ed91dddb7b11267f597d490e16d8f9e70d0b03a Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:23:46 +0800 Subject: [PATCH 2/4] aarch64 used x86_64's numbers, so no directory opened and no file had a kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects, both from one habit: a constant that differs between architectures written once, as though it did not. ⚠️⚠️ THE OPEN FLAGS. `O_DIRECTORY`, `O_NOFOLLOW` and `O_DIRECT` have one set of values on x86_64 and another in the kernel's architecture-independent header, which is what aarch64 uses: x86_64 aarch64 O_DIRECTORY 0200000 040000 O_NOFOLLOW 0400000 0100000 O_DIRECT 040000 0200000 So on aarch64 this asked for O_DIRECT where it meant O_DIRECTORY, and the kernel refuses O_DIRECT on a directory. EVERY DIRECTORY FAILED TO OPEN --- including the two preopens supplied at inception, which are every directory a program above this can reach. Measured: `kal_fs_preopen_count' answered two and both entries reported `kal_err_permission' with a handle of zero. A C library above then had nothing to resolve a name against, so every `open' answered ENOENT and `getcwd' answered "/", which reads as a program started somewhere odd rather than as an implementation that opened nothing. ⚠️⚠️ THE STAT RECORD. The kernel's architecture-independent `struct stat' places the mode immediately after the device and inode; the layout here was neither that nor x86_64's. The mode was read from offset 60, where the kernel writes a group, and the size from 32, where it writes a device number. Measured, the same program on both: x86_64 file: kind=1 size=10 writable=1 link: kind=3 aarch64 file: kind=4 size=0 writable=0 link: kind=4 Every node on aarch64 was "some other kind of thing", of length zero and not writable --- so a C library reported that a file it had just written ten bytes to was not a regular file, and every operation that decides upon a kind decided wrongly. ⭐ THE OFFSETS ARE NOW ASSERTED. A field read from the wrong offset is a wrong answer and not a failure, so the build could not report it; now it can. ⚠️ AND NOTHING IN THIS ECOSYSTEM COULD HAVE CAUGHT EITHER. The conformance suite runs on the machine that builds it, and every hosted machine in this ecosystem's continuous integration is x86_64 or an arm64 Mac --- which uses openkal-macos and a different record again. The aarch64 leg of this implementation was built and never run. Both were found by running it: under qemu-aarch64 the port's own POSIX probe reported seven failures where x86_64 reported none, and after these two it reports one, which is the emulator's own handling of a program that starts another. --- src/sys.h | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/src/sys.h b/src/sys.h index 980dee6..adc01e8 100644 --- a/src/sys.h +++ b/src/sys.h @@ -211,10 +211,44 @@ enum : int { }; // --- constants the kernel defines ------------------------------------------ +// +// ⚠️⚠️ THREE OF THESE ARE NOT THE SAME NUMBER ON BOTH ARCHITECTURES, AND WERE +// WRITTEN AS THOUGH THEY WERE. +// +// `O_DIRECTORY', `O_NOFOLLOW' and `O_DIRECT' have one set of values on x86_64 +// and another in the kernel's architecture-independent header, which is what +// aarch64 uses. The three values below were x86_64's: +// +// x86_64 aarch64 (asm-generic) +// O_DIRECTORY 0200000 040000 +// O_NOFOLLOW 0400000 0100000 +// O_DIRECT 040000 0200000 +// +// So on aarch64 this implementation asked for O_DIRECT where it meant +// O_DIRECTORY. The kernel refuses O_DIRECT on a directory, so EVERY DIRECTORY +// THIS IMPLEMENTATION TRIED TO OPEN FAILED --- including the two preopens it +// supplies at inception, which is every directory a program above it can reach. +// +// ⭐ MEASURED, and the reading is unambiguous: on aarch64 `kal_fs_preopen_count' +// answered two and both entries reported `kal_err_permission' with a handle of +// zero, while the same program on x86_64 reported both directories and opened a +// file in one. A C library above it then had no directory to resolve a name +// against, so every `open' answered ENOENT and `getcwd' answered "/" --- which +// reads as a program started somewhere odd rather than as an implementation +// that opened nothing. +// +// ⚠️ Nothing caught it. The conformance suite is run on x86_64; this package is +// built for aarch64 and the build succeeds, because a wrong constant is a +// number and not a type error. enum : okl_long { o_rdonly = 0, o_wronly = 1, o_rdwr = 2, o_creat = 0100, o_excl = 0200, o_trunc = 01000, o_append = 02000, - o_directory = 0200000, o_cloexec = 02000000, o_nofollow = 0400000, + o_cloexec = 02000000, +#if defined(__x86_64__) + o_directory = 0200000, o_nofollow = 0400000, +#else + o_directory = 040000, o_nofollow = 0100000, +#endif at_fdcwd = -100, at_removedir = 0x200, at_symlink_nofollow = 0x100, prot_read = 1, prot_write = 2, prot_exec = 4, prot_none = 0, map_private = 2, map_anonymous = 0x20, map_stack = 0x20000, @@ -314,17 +348,41 @@ struct kstat { okl_i64 blksize; okl_i64 blocks; #else + // ⚠️⚠️ THE FIELDS OF THIS ARCHITECTURE'S RECORD WERE IN THE WRONG ORDER, AND + // THE BUILD COULD NOT SAY SO. + // + // The kernel's architecture-independent `struct stat' --- which aarch64 uses + // --- places the mode immediately after the device and inode, and the size + // after the device number and one word of padding. The order below was + // neither: the mode was read from offset 60 where the kernel writes a + // group, and the size from 32 where it writes a device number. + // + // ⭐ MEASURED, the same program on both architectures: + // + // x86_64 file: kind=1 size=10 writable=1 link: kind=3 + // aarch64 file: kind=4 size=0 writable=0 link: kind=4 + // + // Every node on aarch64 was "some other kind of thing", of length zero and + // not writable. A C library above it reported that a file it had just + // written ten bytes to was not a regular file --- and every operation that + // decides upon a kind, which is most of `std::filesystem', decided wrongly. + // + // ⚠️ NOTHING IN THIS ECOSYSTEM COULD HAVE CAUGHT IT. The conformance suite + // runs on the machine that builds it, and every hosted machine in this + // ecosystem's continuous integration is x86_64 or an arm64 Mac --- which + // uses openkal-macos and a different record again. The aarch64 leg of THIS + // implementation is built and never run. The offsets are asserted below so + // that the next such error is a build failure rather than a wrong answer. + okl_u32 mode; + okl_u32 nlink; + okl_u32 uid; + okl_u32 gid; okl_u64 rdev; okl_u64 pad1; okl_i64 size; okl_u32 blksize; okl_u32 pad2; okl_i64 blocks; - okl_u32 nlink; - okl_u32 mode; - okl_u32 uid; - okl_u32 gid; - okl_u32 pad0; #endif okl_i64 atime_sec, atime_nsec; okl_i64 mtime_sec, mtime_nsec; @@ -332,6 +390,20 @@ struct kstat { okl_i64 unused[3]; }; +// The kernel writes this record; a field read from the wrong offset is a wrong +// answer and not a failure, so the offsets are stated here rather than trusted. +#if defined(__x86_64__) +static_assert(__builtin_offsetof(kstat, mode) == 24, "x86_64 struct stat"); +static_assert(__builtin_offsetof(kstat, size) == 48, "x86_64 struct stat"); +static_assert(__builtin_offsetof(kstat, mtime_sec) == 88, "x86_64 struct stat"); +#else +static_assert(__builtin_offsetof(kstat, mode) == 16, "asm-generic struct stat"); +static_assert(__builtin_offsetof(kstat, size) == 48, "asm-generic struct stat"); +static_assert(__builtin_offsetof(kstat, mtime_sec) == 88, "asm-generic struct stat"); +#endif +static_assert(__builtin_offsetof(kstat, ino) == 8, "the inode follows the device"); +static_assert(sizeof(kstat) >= 128, "the kernel writes at least this much"); + enum : okl_u32 { s_ifmt = 0170000, s_ifreg = 0100000, s_ifdir = 0040000, s_iflnk = 0120000, }; From 93d5448d129b2c0e3cd8c5269dd541841c34d6d1 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:39:00 +0800 Subject: [PATCH 3/4] The package's own tests, and a README that names versions which exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ THE TESTS WERE NOT UPDATED AND CI IS WHERE THAT SHOWED. Seven of them use the surface directly and every one of them failed to compile against 0.9 --- which is the arrangement working: a package that changes an interface and does not change what examines it has changed nothing that anybody checked. One of them then failed at RUN rather than at compile, and the reason is worth recording: the enquiry that reports a preopen's name now copies it into the caller's buffer, so a call site that passed a null buffer and then read the name compiled and dereferenced zero. The end of an enumeration is likewise the iterator becoming zero rather than a pointer becoming null, because there is no pointer any more. ⚠️⚠️ AND THE README TOLD A READER TO ASK FOR A VERSION FOUR MINOR RELEASES OLD. 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. The specification's own said `openkal = "0.5.1"` while the package was at 0.9.0. ⭐ The staleness is not the finding. That it was INVISIBLE is: the surface is checked against SURFACE.txt, the declarations against both forms, the behaviour against the conformance suite --- and the one line a reader actually types was checked by nobody. `tools/check-readme-versions.sh` in the specification's repository now checks it, and reports `?` rather than passing where it cannot look, so its "no" and its "did not run" do not read the same. 7 passed, 0 failed. --- README.md | 4 +-- tests/conformance_additions.cpp | 20 +++++++-------- tests/conformance_env_time.cpp | 16 +++++++----- tests/conformance_fs.cpp | 39 +++++++++++++++++------------- tests/conformance_process_task.cpp | 14 ++++++++--- tests/conformance_stream.cpp | 13 ++++++---- tests/conformance_v08.cpp | 35 +++++++++++++-------------- 7 files changed, 79 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 59cd0d0..25a446b 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ for Linux, written on the kernel's own system-call interface. ```toml [dependencies] -openkal = "0.5.1" +openkal = "0.9.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.5.1" +openkal-linux = "0.7.0" ``` ## Why it does not use a C library diff --git a/tests/conformance_additions.cpp b/tests/conformance_additions.cpp index 76d8234..7c77e06 100644 --- a/tests/conformance_additions.cpp +++ b/tests/conformance_additions.cpp @@ -34,9 +34,9 @@ bool put(const char* name, const char* text) { const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::truncate; if (kal::fs::open_file(here(), name, std::strlen(name), flags, &f) != kal_ok) return false; - const auto r = kal_stream_write(kal_stream{kal_fs_stream(f)}, text, std::strlen(text)); + const kal_intptr r = kal_stream_write(kal_fs_stream(f), text, std::strlen(text)); kal_fs_close_file(f); - return r.e == kal_ok; + return r == static_cast(std::strlen(text)); } // Reads a whole file into the buffer and reports its length, or -1. @@ -44,9 +44,9 @@ long get(const char* name, char* buf, kal_uintptr cap) { kal_file f{}; if (kal::fs::open_file(here(), name, std::strlen(name), kal::fs::open::read, &f) != kal_ok) return -1; - const auto r = kal_stream_read(kal_stream{kal_fs_stream(f)}, buf, cap); + const kal_intptr r = kal_stream_read(kal_fs_stream(f), buf, cap); kal_fs_close_file(f); - return r.e == kal_ok ? static_cast(r.n) : -1; + return r >= 0 ? static_cast(r) : -1; } } // namespace @@ -86,7 +86,7 @@ int main() { // success for every call. kal_u64 at = 0; kal_fs_seek(a, 0, kal::fs::seek_set, &at); - kal_stream_write(kal_stream{kal_fs_stream(a)}, "two", 3); + kal_stream_write(kal_fs_stream(a), "two", 3); kal_fs_close_file(a); } len = get(name, buf, sizeof buf); @@ -99,20 +99,20 @@ int main() { check(kal::fs::open_file(here(), name, n, kal::fs::open::write, &t) == kal_ok, "a file opens for setting its length"); check(kal_fs_truncate(t, 2) == kal_ok, "the length is set"); - kal_node_info info{}; - check(kal_fs_file_info(t, &info) == kal_ok, "an open file is enquired about"); + kal_node_info info{}; info.self_size = sizeof info; + check(kal_fs_file_info(t, kal::fs::field::all, &info) == kal_ok, "an open file is enquired about"); check(info.size == 2, "the enquiry reports the length that was set"); check(info.kind == kal_node_file, "the enquiry reports what the handle refers to"); check(kal_fs_truncate(t, 9) == kal_ok, "the length is extended"); - check(kal_fs_file_info(t, &info) == kal_ok && info.size == 9, + check(kal_fs_file_info(t, kal::fs::field::all, &info) == kal_ok && info.size == 9, "extending reports the larger length"); kal_fs_close_file(t); } // --- absence is an answer, and is distinguishable ------------------------ kal_fs_remove(here(), name, n); - kal_node_info gone{}; - check(kal_fs_info(here(), name, n, &gone) == kal_ok && gone.kind == kal_node_absent, + kal_node_info gone{}; gone.self_size = sizeof gone; + check(kal_fs_info(here(), name, n, 0, kal::fs::field::all, &gone) == kal_ok && gone.kind == kal_node_absent, "enquiry about a name that does not exist is answered"); check(kal::fs::open_file(here(), name, n, kal::fs::open::read, &f) == kal_err_not_found, "opening a name that does not exist reports that it does not exist"); diff --git a/tests/conformance_env_time.cpp b/tests/conformance_env_time.cpp index 9c53263..7dbf27e 100644 --- a/tests/conformance_env_time.cpp +++ b/tests/conformance_env_time.cpp @@ -20,16 +20,20 @@ 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 --- which is what + // lets a caller size a buffer before it has one. + 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 ff2731a..b612d29 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -25,38 +25,40 @@ 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"); kal_u64 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 +67,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: a name is + // copied into the caller's buffer, so there is no pointer to be null. + if (iter == 0) break; if (len == 5 && name[0] == 'i') found = true; } check(found, "enumeration finds the entry"); @@ -87,7 +92,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 ee867c8..1925594 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -37,12 +37,18 @@ 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; + // ⭐ THE NAME IS COPIED INTO A BUFFER HERE, which is what the operation now + // does: it answered with a pointer into the implementation's own storage, + // which is meaningful only while the implementation shares this address + // space. + 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"); diff --git a/tests/conformance_v08.cpp b/tests/conformance_v08.cpp index dae0ea8..582ade8 100644 --- a/tests/conformance_v08.cpp +++ b/tests/conformance_v08.cpp @@ -100,13 +100,13 @@ void net_section() { // operations. That this interface adds no transfer operation of its own is // the property being observed. const char msg[] = "openkal"; - const auto w = kal_stream_write(cs, msg, sizeof msg - 1); - check(w.e == kal_ok && w.n == sizeof msg - 1, + const kal_intptr w = kal_stream_write(cs, msg, sizeof msg - 1); + check(w == static_cast(sizeof msg - 1), "a connection carries bytes through the stream operations"); char buf[16] = {}; - const auto r = kal_stream_read(ss, buf, sizeof buf); - check(r.e == kal_ok && r.n == sizeof msg - 1 && + const kal_intptr r = kal_stream_read(ss, buf, sizeof buf); + check(r == static_cast(sizeof msg - 1) && std::memcmp(buf, msg, sizeof msg - 1) == 0, "the bytes read are the bytes written"); @@ -114,9 +114,8 @@ void net_section() { check(kal::net::shutdown(c.c, kal::net::shut::write) == kal_ok, "a claimed half-closure is performed"); char eof[4] = {}; - const auto e = kal_stream_read(ss, eof, sizeof eof); - check(e.e == kal_ok && e.n == 0, - "the peer observes end of input after a half-closure"); + const kal_intptr e = kal_stream_read(ss, eof, sizeof eof); + check(e == 0, "the peer observes end of input after a half-closure"); } // An endpoint whose length this implementation does not know is refused @@ -150,12 +149,12 @@ void datagram_section() { const char msg[] = "openkal"; const auto w = kal::datagram::send_to(tx.d, msg, sizeof msg - 1, loopback(bound.ep.port)); - check(w.e == kal_ok && w.n == sizeof msg - 1, + check(w == static_cast(sizeof msg - 1), "a message is sent whole and the count is the length given"); char buf[16] = {}; const auto got = kal::datagram::recv_from(rx.d, buf, sizeof buf); - check(got.r.e == kal_ok && got.r.n == sizeof msg - 1 && + check(got.n == static_cast(sizeof msg - 1) && std::memcmp(buf, msg, sizeof msg - 1) == 0, "the message received is the message sent"); check(got.from.addr_len == 4, "the sender of a received message is reported"); @@ -193,7 +192,7 @@ void space_section() { // openkal.timeout void timeout_section() { - check(kal_timeout_granularity_ns > 0, + check(kal_timeout_granularity() > 0, "the granularity is a positive number of nanoseconds"); // A bounded read of a listener that nobody connects to must expire rather @@ -208,8 +207,8 @@ void timeout_section() { } // A transfer of zero bytes does not wait and is not bounded. - const auto w = kal::timeout::write(kal_stdout(), "", 0, 1); - check(w.e == kal_ok, "a bounded transfer of zero bytes succeeds"); + const kal_intptr w = kal::timeout::write(kal_stdout(), "", 0, 1); + check(w >= 0, "a bounded transfer of zero bytes succeeds"); } // The three operations openkal 0.8 adds to openkal.process. @@ -228,13 +227,13 @@ void process_additions_section() { if (rc != kal_ok) return; const char msg[] = "through the channel"; - const auto w = kal_stream_write(theirs, msg, sizeof msg - 1); - check(w.e == kal_ok && w.n == sizeof msg - 1, + const kal_intptr w = kal_stream_write(theirs, msg, sizeof msg - 1); + check(w == static_cast(sizeof msg - 1), "the far end of a channel accepts bytes"); char buf[64] = {}; - const auto r = kal_stream_read(mine, buf, sizeof buf); - check(r.e == kal_ok && r.n == sizeof msg - 1 && + const kal_intptr r = kal_stream_read(mine, buf, sizeof buf); + check(r == static_cast(sizeof msg - 1) && std::memcmp(buf, msg, sizeof msg - 1) == 0, "the near end reads what the far end wrote"); @@ -242,8 +241,8 @@ void process_additions_section() { // the far end after a spawn never observes it, which is the deadlock this // pair invites and the reason the release is declared beside the operation. kal_process_channel_close(theirs); - const auto eof = kal_stream_read(mine, buf, sizeof buf); - check(eof.e == kal_ok && eof.n == 0, + const kal_intptr eof = kal_stream_read(mine, buf, sizeof buf); + check(eof == 0, "closing the far end is observed as end of input on the near one"); kal_process_channel_close(mine); From 24a6556e2f9ce0119f40cd949298a5cd3ebb6cd7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:40:22 +0800 Subject: [PATCH 4/4] fix: one C library passes the vectors to .init_array and the other does not glibc calls every .init_array entry with (argc, argv, envp). musl calls them with NO ARGUMENTS. src/env.cpp declared a constructor taking three and recorded what arrived, which under musl is whatever the argument registers happened to hold -- so every program above this implementation and a musl C library was told its argument count was a text address, and the first enquiry after the count dereferenced a small integer as a pointer. Found by running this package's tests for aarch64, where it reads as an architecture defect. It is not, and the control that separates the two is x86_64-linux-musl: target argc argv envp x86_64-linux-gnu 1 x86_64-linux-musl 0x4004c2 1 aarch64-linux-musl 0x405ee4 1 Both musl rows are shifted by one and the glibc row is not, so the axis is the C library and not the machine. Running only aarch64 would have attributed it to the machine, and the fix would have been wrong. The arguments are now checked rather than believed -- argv[argc] must be the terminator, argc must be a count -- and where they do not hold the vectors are recovered from `environ' by walking back over argv's terminator to the slot that holds the count, which must equal the number of entries actually found. Where even that does not hold, nothing is recorded and the program is told it has no arguments, which is an answer rather than a fault. `environ' is a WEAK reference and the independence check now says so as a rule rather than as an exception: this one name is permitted only when its type letter is weak. It is admissible where `puts' is not because a call into the runtime a program supplied can re-enter this implementation without bound and a pointer executes nothing, and because weak means a program with no C library still links. A strong reference to the same name would make one required silently, and the check now fails on it -- measured, both ways. Verified: 7 tests pass on x86_64-linux-gnu, x86_64-linux-musl and aarch64-linux-musl. --- .github/workflows/ci.yml | 37 ++++++++++++++++++--- src/env.cpp | 71 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8f82c0..a743899 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,14 +265,41 @@ jobs: # __libc_start_main and main are the hand-over, and are undefined here # by construction. permitted='^(memcpy|memmove|memset|memcmp|__libc_start_main|main|_GLOBAL_OFFSET_TABLE_|kal_[a-z_]+|__init_array_start|__init_array_end|__preinit_array_start|__preinit_array_end|_ZN3okl.*)$' + + # ⭐⭐ ONE NAME IS PERMITTED ONLY IF IT IS WEAK, AND THE WEAKNESS IS + # THE WHOLE OF THE PERMISSION. + # + # `environ' is how src/env.cpp recovers the vectors the kernel placed + # on the stack when the C library above did not pass them --- glibc + # calls every `.init_array' entry with (argc, argv, envp) and musl + # calls them with none, so what arrived there was register residue. + # + # It is admissible where `puts' is not, and the difference is not that + # it is smaller. This check exists because a CALL into the runtime a + # program supplied resolves to the program's and can re-enter this + # implementation without bound. A pointer executes nothing. And being + # WEAK it is null in a program that has no C library, so it does not + # make one required --- which a strong reference to the same name + # would, silently, and is why the type letter is checked and not just + # the name. + weak_permitted='^environ$' bad=0 - for s in $(nm --undefined-only $objs | awk '{print $2}' | sort -u); do - [ -n "$s" ] || continue - if ! printf '%s\n' "$s" | grep -qE "$permitted"; then - echo "the implementation references a symbol it must not: $s" >&2 - bad=1 + nm --undefined-only $objs | awk '{ print $1, $2 }' | sort -u | + while read -r kind name; do + [ -n "$name" ] || continue + printf '%s\n' "$name" | grep -qE "$permitted" && continue + if printf '%s\n' "$name" | grep -qE "$weak_permitted"; then + case "$kind" in + w|v) continue ;; + *) echo "::error::$name is permitted only as a weak reference, and this one is '$kind'" >&2 ;; + esac + else + echo "the implementation references a symbol it must not: $name" >&2 fi + echo bad >> "$RUNNER_TEMP/independence.bad" done + [ -s "$RUNNER_TEMP/independence.bad" ] && bad=1 + rm -f "$RUNNER_TEMP/independence.bad" test "$bad" -eq 0 echo "the implementation references no C library symbol" diff --git a/src/env.cpp b/src/env.cpp index 2cc61ef..d513aa2 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -34,8 +34,77 @@ okl_ulong auxval(okl_ulong key) { } // namespace okl namespace { + +// ⚠️⚠️ ONE C LIBRARY PASSES THESE AND ANOTHER DOES NOT, AND THE ONE THAT DOES +// NOT IS THE ONE THIS PACKAGE EXISTS TO SIT BENEATH. +// +// glibc calls every `.init_array' entry with (argc, argv, envp). musl calls +// them with NO ARGUMENTS. A function declared to take three therefore receives +// whatever the argument registers happened to hold, and this one recorded it. +// +// ⭐ MEASURED 2026-08-29, WITH THE CONTROL THAT SEPARATES THE TWO EXPLANATIONS. +// It was found by running the tests for aarch64, where the first enquiry after +// the count faulted --- which reads as an architecture defect. It is not: +// +// target argc argv envp +// x86_64-linux-gnu 1 +// x86_64-linux-musl 0x4004c2 1 +// aarch64-linux-musl 0x405ee4 1 +// +// Both musl rows are shifted by one and the glibc row is not, so the axis is +// the C library. Running only aarch64 would have attributed it to the machine. +// +// So the arguments are CHECKED rather than believed, and where they do not hold +// the vectors are recovered from the one handle both libraries publish. +bool plausible(int argc, char* const* argv, char* const* envp) { + if (argc < 0 || argc > 65536) return false; + if (argv == nullptr) return false; + if (envp == nullptr) return false; + if (argv[argc] != nullptr) return false; // argv is terminated at argc + if (argc > 0 && argv[0] == nullptr) return false; + return true; +} + +// ⚠️ WEAK, AND DATA RATHER THAN A CALL. The independence check in this package +// forbids reaching for the C library's names, because a CALL into the runtime a +// program supplied would resolve to the program's and could re-enter this +// implementation without bound. A pointer cannot: it is read once, it executes +// nothing, and being weak it is null in a program that has no C library --- in +// which case this implementation supplied `_start' and recorded the vectors +// there, and this path is not taken. +extern "C" char** environ __attribute__((weak)); + +// The initial stack, whose shape is the ELF ABI's rather than any library's: +// +// argc argv[0] .. argv[argc-1] NULL envp[0] .. NULL auxv... +// +// so from `envp' the argument vector is reached by walking back over its +// terminator. The walk is CHECKED and not trusted: the count found in the slot +// below argv[0] must equal the number of entries actually there, which a run of +// unrelated stack words does not satisfy. Where it does not hold, nothing is +// recorded and the program is told it has no arguments -- which is an answer, +// and is what clause 7.7 asks of an implementation that cannot know. +bool recover(char*** argv_out, int* argc_out, char*** envp_out) { + char** e = environ; + if (e == nullptr || e[-1] != nullptr) return false; + for (long k = 0; k <= 65536; ++k) { + auto* slot = reinterpret_cast(e - 2 - k); + if (*slot != k) continue; + char** candidate = e - 1 - k; + bool holds = true; + for (long i = 0; i < k && holds; ++i) if (candidate[i] == nullptr) holds = false; + if (!holds || candidate[k] != nullptr) continue; + *argv_out = candidate; *argc_out = static_cast(k); *envp_out = e; + return true; + } + return false; +} + [[gnu::constructor(101)]] void capture(int argc, char** argv, char** envp) { - if (okl::g_argv == nullptr) okl::record(argc, argv, envp); + if (okl::g_argv != nullptr) return; + if (plausible(argc, argv, envp)) { okl::record(argc, argv, envp); return; } + char** rargv = nullptr; char** renvp = nullptr; int rargc = 0; + if (recover(&rargv, &rargc, &renvp)) okl::record(rargc, rargv, renvp); } } // namespace