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
112 changes: 112 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1180,3 +1180,115 @@ jobs:
run: |
./cross-hello-arm64 | tee run.log
grep -q -- '-- failures: 0 --' run.log

# ── AN OK IMAGE (`<arch>-openkal`, openkal SPEC clause 12) ─────────────────
#
# The image form of this library: no implementation beneath it (the loader
# that runs the image holds one), `dlopen' over libokld, the entry SPEC 12.2
# names. Built once on Linux for both architectures, by the mcpp that has the
# target (MCPP_SOURCE_REF until it is released), and the same bytes then run
# through okrun on four systems.
#
# image-dlopen eleven observations of placing another image
# image-early-constructor the library is up before a program's constructor
images:
name: ok images (built once)
runs-on: ubuntu-24.04
timeout-minutes: 90
env:
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF || 'release/2026.10.26.2' }}
XLINGS_VERSION: v2026.10.26.1
steps:
- uses: actions/checkout@v4
- name: The specification, libokld and the builtins, from the same-named branch
run: bash tools/working-trees.sh '${{ github.head_ref || github.ref_name }}' x86_64-openkal
- name: Install xlings
run: |
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \
https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: The mcpp that has the target
run: |
xlings update > /dev/null 2>&1 || true
xlings install mcpp -y -g
mcpp self config --mirror GLOBAL
src="$RUNNER_TEMP/mcpp-src"
git clone --quiet --depth 1 --branch "$MCPP_SOURCE_REF" https://github.com/mcpp-community/mcpp.git "$src"
rm -f "$src/.xlings.json"
( cd "$src" && mcpp build --release )
built=$(find "$src/target" -type f -name mcpp | head -1)
[ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; }
echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH"
"$built" --version
- name: The images
run: |
set -euo pipefail
for arch in x86_64 aarch64; do
mkdir -p "out/$arch"
( cd examples/image-dlopen/plugin && mcpp build --target "$arch-openkal" )
( cd examples/image-dlopen && mcpp build --target "$arch-openkal" )
( cd examples/image-early-constructor && mcpp build --target "$arch-openkal" )
cp examples/image-dlopen/plugin/target/"$arch"-openkal/*/bin/libplug.okso "out/$arch/"
cp examples/image-dlopen/target/"$arch"-openkal/*/bin/image-dlopen.oke "out/$arch/"
cp examples/image-early-constructor/target/"$arch"-openkal/*/bin/image-early-constructor.oke "out/$arch/"
done
(cd out && sha256sum */* | tee SHA256SUMS)
- uses: actions/upload-artifact@v4
with: { name: musl-images, path: out }

images-run:
name: ok images on ${{ matrix.host }}
needs: images
runs-on: ${{ matrix.os }}
timeout-minutes: 45
env:
XLINGS_VERSION: v2026.10.26.1
strategy:
fail-fast: false
matrix:
include:
- { host: linux-x86_64, os: ubuntu-24.04, arch: x86_64 }
- { host: linux-aarch64, os: ubuntu-24.04-arm, arch: aarch64 }
- { host: macos-arm64, os: macos-15, arch: aarch64 }
- { host: windows-x86_64, os: windows-2022, arch: x86_64 }
defaults:
run:
shell: bash
steps:
- uses: actions/download-artifact@v4
with: { name: musl-images, path: out }
- name: Install xlings (Unix)
if: runner.os != 'Windows'
run: |
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \
https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: Install xlings (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: okrun, built here
run: |
xlings install mcpp -y -g
mcpp self config --mirror GLOBAL
mcpp toolchain install llvm 23.1.3
mcpp toolchain default llvm@23.1.3
git clone --quiet --depth 1 --branch openkal-0.17.0 https://github.com/mcpplibs/openkal-image.git "$RUNNER_TEMP/oi"
( cd "$RUNNER_TEMP/oi/tools" && mcpp build --release )
okrun=$(find "$RUNNER_TEMP/oi/target" -path '*bin*' \( -name okrun -o -name okrun.exe \) -type f | head -1)
[ -n "$okrun" ] || { echo "::error::okrun did not build"; exit 1; }
echo "OKRUN=$okrun" >> "$GITHUB_ENV"
- name: The same bytes
run: |
set -euo pipefail
a='${{ matrix.arch }}'
if command -v sha256sum > /dev/null; then (cd out && sha256sum -c SHA256SUMS)
else (cd out && shasum -a 256 -c SHA256SUMS); fi
"$OKRUN" "out/$a/image-dlopen.oke" "out/$a/libplug.okso" | tr -d '\r' | tee dlopen.log
grep -q -- '-- failures: 0 --' dlopen.log || { echo "::error::image-dlopen"; exit 1; }
"$OKRUN" "out/$a/image-early-constructor.oke" | tr -d '\r' | tee early.log
grep -q 'the C library was up before the constructor' early.log || { echo "::error::image-early-constructor"; exit 1; }
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ lesser of the two, and the row returns when the schema carries `targets`.
| the identity of a node | `st_dev` and `st_ino` are the implementation's answer where it has one, and **zero for both where it has none** | They were the constants 0 and 1, so every file compared equal to every other: `std::filesystem::equivalent` on two separately created files answered `true` **with no error**. openkal 0.9 carries an identity and reports whether it knows one; a caller must not read two zeroes as sameness, which is why nothing is invented for an implementation that cannot distinguish nodes. |
| ownership | `chown` reports `ENOSYS`; `stat` reports 1000 for both | as the row above: a capability-oriented environment has no principal for an owner to name. |
| a mode given at creation | `open(…, O_CREAT, 0600)` and `mkdir(path, 0700)` **succeed** and `stat` afterwards reports 0666 and 0777 | the row above, in the one place where it does not read as a refusal. openkal opens a file for a purpose and not for an audience, so the argument has nowhere to go. Refusing every mode but the one `stat` will report would refuse nearly every program; what a caller can rely on instead is stated below. |
| processor clocks | `CLOCK_PROCESS_CPUTIME_ID` and `clock()` are the program's processor time (**since 0.22.0**; before, every clock but the wall clock was the monotonic one, and time asleep counted as work); `CLOCK_THREAD_CPUTIME_ID` reports `EINVAL` | openkal 0.17 reports a program's processor time in `kal_program_info` and has no context's own; a clock that answered with another quantity would be answered wrongly rather than refused. |
| entropy | `getrandom` reports `ENOSYS` where the backend declines `openkal.random` | openkal has no source of one to require, and this port does not invent one. The allocator's cookie and the stack canary are derived from the clock and from an address; neither is a security property here. |
| a signal delivered anywhere | `raise` and `kill` perform a signal's **default action** and nothing else: terminating signals end the program, ignored ones succeed, stopping ones report `ENOSYS`, and musl's own three (32, 33, 34) are refused, so `pthread_cancel` reports `ENOSYS` | there is no delivery, so there is no handler to reach; what remains of a signal is what it does when no handler exists. `abort` reaches `kal_abort`, which raises the signal on Linux and ends with a distinguished status elsewhere — a parent can tell an abnormal end from an ordinary one on every system. |
| an immediate answer about a started program | `waitpid(…, WNOHANG)` returns without the program having finished, but may wait up to one polling interval of the implementation beneath (one millisecond on Linux) | `kal_timeout_wait_process` takes a bound and openkal spells "no bound" as zero, so a caller that does not want to wait asks for the smallest bound there is. An environment rounds a bound up to what its clock can distinguish; a bound shorter than the clock is a promise no environment can keep. |
Expand Down Expand Up @@ -633,6 +634,30 @@ rather than beneath because openkal is deliberately literal about names: it
passes on the name it was given and does not know that a program is a kind of
file.

## As the C library of an ok image (0.22.0)

For the target `<arch>-openkal` (openkal SPEC clause 12) this library is built
into an **ok image**: one artifact per architecture that `okrun` runs on
Linux, macOS and Windows. Nothing of an implementation is beneath it in the
image --- the loader holds the one implementation of the process and binds the
image's `kal_*` imports to it --- so the image form differs from the others in
four places, all in `port/src/image`:

| Item | The image form |
|---|---|
| entry | `int ok_main(void)`, the entry SPEC 12.2 names: the library comes up (if its prioritised constructor has not already brought it up), `main` runs, then the `atexit` handlers and the flush of every stream; the loader runs the finalisers and ends with the status |
| thread-local storage | emulated (SPEC 12.2): `__emutls_get_address` keeps each variable under a key of this library's own |
| `dlopen`, `dlsym`, `dladdr`, `dlclose`, `dlerror` | over libokld (`openkal-ld`), filled from `openkal.exec`; a placed image's `kal_*` imports are bound through a table of every name of `SURFACE.txt`, held weakly so that it requires no interface (SPEC 12.4), then to this image's exports and to images placed with `RTLD_GLOBAL` |
| `dl_iterate_phdr` | this image, then every placed one, so an unwinder finds each image's frames |

The compiler-rt builtins come from `llvm.compiler-rt-builtins` (22.1.8.6), built
with the image's flags, behind the default feature `image-builtins`: on AArch64
a `long double` is 128 bits and `printf` reaches its routines. A package that
supplies the layer itself (`openkal-llvm-runtime`) depends on this one with
`default-features = false`, since a build has one compiler runtime. `examples/image-dlopen` and
`examples/image-early-constructor` are built once on Linux for both
architectures and run through `okrun` on four systems in CI.

## What runs above it

| | |
Expand Down
23 changes: 23 additions & 0 deletions build.mcpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,29 @@ int main() {
mcpp::cflag(("-DOKM_VERSION=\\\"" + v + "\\\"").c_str());
}

// AN OK IMAGE'S `dlopen' BINDS A PLACED IMAGE'S `kal_*' IMPORTS BY NAME
// (port/src/image), so the names are the specification's own list, read
// from the openkal this package is built against at every build: a table
// written here by hand would fall behind the specification it names.
if (os == "openkal") {
const std::string dep = env_or_empty("MCPP_DEP_OPENKAL_DIR");
const std::string out = env_or_empty("MCPP_OUT_DIR");
const std::filesystem::path surface = std::filesystem::path(dep) / "SURFACE.txt";
std::ifstream in(surface);
if (dep.empty() || out.empty() || !in) {
std::printf("mcpp:warning=openkal's SURFACE.txt cannot be read; dlopen binds no kal_* name\n");
}
std::ofstream o(std::filesystem::path(out) / "okm_kal_names.inc");
o << "/* Generated by openkal-musl's build.mcpp from " << surface.generic_string() << ". */\n";
std::string line;
while (std::getline(in, line)) {
while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back();
if (line.rfind("kal_", 0) == 0) o << "OKM_KAL(" << line << ")\n";
}
std::printf("mcpp:include-dir=%s\n", out.c_str());
mcpp::rerun_if_changed(surface.string().c_str());
}

if (fam != "gcc") return 0;

// ELF NEEDS IT TOO, AND FOR THE SAME REASON THE PE BLOCK DID. The Linux
Expand Down
17 changes: 17 additions & 0 deletions examples/image-dlopen/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# An ok image that places another with `dlopen' (target `<arch>-openkal';
# openkal SPEC clause 12). The image this program places is built from
# plugin/, an image of its own with no C library, and is named on the command
# line: it is not linked to this one.
[package]
name = "image-dlopen"
version = "0.1.0"

[dependencies]
openkal-musl = { path = "../.." }

[build-dependencies]
openkal-tools = { git = "https://github.com/mcpplibs/openkal-image.git", branch = "openkal-0.17.0", tools = ["okld", "okrun"] }

[targets.image-dlopen]
kind = "bin"
main = "src/main.c"
11 changes: 11 additions & 0 deletions examples/image-dlopen/plugin/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# The image image-dlopen places: no C library, its exports marked, its imports
# `kal_*' alone. A constructor and a destructor say when the loader ran them.
[package]
name = "plug"
version = "0.1.0"

[build-dependencies]
openkal-tools = { git = "https://github.com/mcpplibs/openkal-image.git", branch = "openkal-0.17.0", tools = ["okld", "okrun"] }

[targets.plug]
kind = "shared"
23 changes: 23 additions & 0 deletions examples/image-dlopen/plugin/src/plug.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* An image that another places with `dlopen'. It holds no C library: its
* imports are `kal_*' alone, and what it exports is marked. */
typedef unsigned long kal_uintptr;
typedef long kal_intptr;
struct kal_stream { kal_uintptr h; };
struct kal_stream kal_stdout(void);
kal_intptr kal_stream_write(struct kal_stream, const void*, kal_uintptr);

#define EXPORT __attribute__((visibility("default")))

EXPORT int plug_ready;
EXPORT void (*plug_on_close)(void);

__attribute__((constructor)) static void up(void) { plug_ready = 1; }
__attribute__((destructor)) static void down(void) { if (plug_on_close) plug_on_close(); }

EXPORT int plug_twice(int x) { return 2 * x; }
EXPORT int plug_call(int (*callback)(int), int v) { return callback(v) + 1; }
EXPORT long plug_say(void)
{
static const char line[] = "plugin: written through kal_stream_write\n";
return kal_stream_write(kal_stdout(), line, sizeof line - 1);
}
70 changes: 70 additions & 0 deletions examples/image-dlopen/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* `dlopen' in an ok image: eleven observations of libokld placing another
* image beneath openkal-musl. The image to place is the first argument. */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdio.h>
#include <string.h>

static int failures;
static void check(int held, const char* what)
{
printf("%s %s\n", held ? " ok " : " FAIL", what);
if (!held) failures++;
}

static int doubled_by_host(int v) { return v * 10; }
static int closed;
static void on_close(void) { closed = 1; }

struct seen { int objects; int plugin; };
static int count(struct dl_phdr_info* info, size_t size, void* data)
{
(void)size;
struct seen* s = data;
s->objects++;
if (info->dlpi_name && strstr(info->dlpi_name, "libplug")) s->plugin = 1;
return 0;
}

int main(int argc, char** argv)
{
if (argc < 2) { puts("usage: image-dlopen <libplug.okso>"); return 2; }

check(dlopen("no-such-image.okso", RTLD_NOW) == 0, "an image that is not there is not placed");
const char* why = dlerror();
check(why && strstr(why, "no-such-image"), "dlerror names it");

void* h = dlopen(argv[1], RTLD_NOW);
if (!h) printf(" (dlerror: %s)\n", dlerror());
check(h != 0, "the image is placed");
if (!h) { printf("-- failures: %d --\n", failures); return 1; }

int (*twice)(int) = (int (*)(int))dlsym(h, "plug_twice");
check(twice != 0, "dlsym finds an export");
check(twice && twice(21) == 42, "the export is called and returns");
int* ready = dlsym(h, "plug_ready");
check(ready && *ready == 1, "the image's constructor ran when it was placed");
check(dlsym(h, "plug_absent") == 0 && dlerror() != 0, "a name it does not export is reported");

Dl_info info;
check(dladdr((void*)twice, &info) && info.dli_fname && strstr(info.dli_fname, "libplug")
&& info.dli_fbase != 0, "dladdr names the image an address is in");

struct seen s = { 0, 0 };
dl_iterate_phdr(count, &s);
check(s.objects >= 2 && s.plugin, "dl_iterate_phdr reports this image and the placed one");

int (*call)(int (*)(int), int) = (int (*)(int (*)(int), int))dlsym(h, "plug_call");
check(call && call(doubled_by_host, 4) == 41, "the placed image calls back into this one");

long (*say)(void) = (long (*)(void))dlsym(h, "plug_say");
check(say && say() > 0, "the placed image's kal_* import is bound");

void (**hook)(void) = dlsym(h, "plug_on_close");
if (hook) *hook = on_close;
check(dlclose(h) == 0 && closed, "dlclose runs its destructor");

printf("-- failures: %d --\n", failures);
return failures ? 1 : 0;
}
15 changes: 15 additions & 0 deletions examples/image-early-constructor/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# The C library is up before the program's own constructors run: the loader
# runs an image's initialisers before its entry (openkal SPEC 12.3).
[package]
name = "image-early-constructor"
version = "0.1.0"

[dependencies]
openkal-musl = { path = "../.." }

[build-dependencies]
openkal-tools = { git = "https://github.com/mcpplibs/openkal-image.git", branch = "openkal-0.17.0", tools = ["okld", "okrun"] }

[targets.image-early-constructor]
kind = "bin"
main = "src/main.c"
23 changes: 23 additions & 0 deletions examples/image-early-constructor/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* A constructor that formats, allocates and reads the environment before
* `main': the loader runs it before the image's entry, so the C library has to
* be up by then (its own initialiser runs first). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char* g_early;

__attribute__((constructor)) static void early(void)
{
g_early = malloc(64);
snprintf(g_early, 64, "early %d %s", 42, getenv("PATH") ? "path" : "nopath");
}

int main(int argc, char** argv)
{
(void)argv;
const int ok = g_early && strncmp(g_early, "early 42 ", 9) == 0 && argc >= 1;
printf("%s\n", g_early ? g_early : "(no early value)");
printf("early-constructor: %s\n", ok ? "the C library was up before the constructor" : "FAIL");
return ok ? 0 : 1;
}
24 changes: 24 additions & 0 deletions examples/posix/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,30 @@ int main(int argc, char **argv, char **envp) {
check(el >= 20000000, "the monotonic clock advanced by at least the requested interval");
check(time(NULL) > 1700000000, "the wall clock reports a plausible time");

/* The processor clock counts work, not time asleep: the program slept
* 200 ms above and spins about as long here. */
{
struct timespec c0, c1, s0, s1;
const int had = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &c0) == 0;
check(had, "the processor clock is read");
struct timespec nap = { 0, 200 * 1000 * 1000 };
nanosleep(&nap, NULL);
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &c1);
const long long slept = (long long)(c1.tv_sec - c0.tv_sec) * 1000000000 + (c1.tv_nsec - c0.tv_nsec);
check(had && slept < 100000000, "time asleep is not processor time");
clock_gettime(CLOCK_MONOTONIC, &s0);
volatile unsigned long long sink = 0;
do {
for (int i = 0; i < 100000; ++i) sink = sink + 1;
clock_gettime(CLOCK_MONOTONIC, &s1);
} while ((s1.tv_sec - s0.tv_sec) * 1000000000LL + (s1.tv_nsec - s0.tv_nsec) < 200000000LL);
struct timespec c2;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &c2);
const long long worked = (long long)(c2.tv_sec - c1.tv_sec) * 1000000000 + (c2.tv_nsec - c1.tv_nsec);
check(had && worked > 50000000, "and work is");
check(clock() != (clock_t)-1, "clock() reports the processor time");
}

/* threads and mutexes: contended */
pthread_t th[4];
counter = 0;
Expand Down
Loading
Loading