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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-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"

Expand All @@ -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
Expand Down
24 changes: 12 additions & 12 deletions src/datagram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,23 @@ 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,
reinterpret_cast<okl_long>(buf),
static_cast<okl_long>(len), 0,
reinterpret_cast<okl_long>(&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
Expand All @@ -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<kal_uintptr>(r);
return { n, n == len ? kal_ok : kal_err_io };
return n == len ? static_cast<kal_intptr>(n) : -kal_err_io;
}
}

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

okl::ksockaddr_storage ss{};
okl_long addrlen = static_cast<okl_long>(sizeof ss);
Expand All @@ -115,7 +115,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
reinterpret_cast<okl_long>(&ss),
reinterpret_cast<okl_long>(&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
Expand All @@ -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<kal_uintptr>(r), kal_ok };
return static_cast<kal_intptr>(r);
}
}

Expand All @@ -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"
121 changes: 103 additions & 18 deletions src/env.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stack> <stack>
// x86_64-linux-musl 0x4004c2 1 <stack>
// aarch64-linux-musl 0x405ee4 1 <stack>
//
// 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<long*>(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<int>(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<kal_intptr>(n);
}
} // namespace

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

const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) {
if (index >= static_cast<kal_uintptr>(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<kal_uintptr>(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;
}

}
6 changes: 5 additions & 1 deletion src/exec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading
Loading