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/.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/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/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..d513aa2 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -34,55 +34,140 @@ 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 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..adc01e8 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, @@ -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, @@ -259,6 +293,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; @@ -273,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; @@ -291,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, }; @@ -433,10 +546,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; +} + +} 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);