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
69 changes: 69 additions & 0 deletions .github/actions/mtest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: mtest
description: >
The mtest engine the conformance suite reports to: the release the workflow pins (MTEST_VERSION), an
xlings package (xim:mtest) that the index verifies, else the release bundle checked against MTEST_SHA256;
MTEST names its launcher. A job that has no xlings yet gets the pinned one first (XLINGS_VERSION).
runs:
using: composite
steps:
- name: xlings, where the job has none
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
command -v xlings > /dev/null 2>&1 && exit 0
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: xlings, where the job has none (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
if (Get-Command xlings -ErrorAction SilentlyContinue) { exit 0 }
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# xlings by the configured mirror (GLOBAL), then by CN (GitCode first), each with a refreshed index
# (a cached index can predate the release, and xlings keeps it when an anonymous refresh fails); else
# the release bundle itself, checked against MTEST_SHA256: the web address, then the API's, which
# answered 200 while the web one answered 504 (GitHub's 2026-11-09 incident).
launcher=""
for mirror in GLOBAL CN; do
xlings config --mirror "$mirror" > /dev/null 2>&1 || true
xlings update > /dev/null 2>&1 || true
if xlings install "mtest@$MTEST_VERSION" -y -g; then launcher=$(command -v mtest); break; fi
done
# The package's own launcher, not the xvm shim: on Windows the shim runs the package's alias through
# cmd, which reads `&` in an argument (`--tag "on:win32-x64&release"`) as a second command.
exe=''; [ "$RUNNER_OS" != Windows ] || exe='.exe'
own="${XLINGS_HOME:-$HOME/.xlings}/data/xpkgs/xim-x-mtest/$MTEST_VERSION/bin/mtest$exe"
if [ -n "$launcher" ] && [ -f "$own" ]; then launcher="$own"; fi
xlings config --mirror GLOBAL > /dev/null 2>&1 || true
if [ -z "$launcher" ]; then
echo "::warning::xlings could not install mtest $MTEST_VERSION; taking the release itself"
archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz"
temp="$RUNNER_TEMP"; python=python3
if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi
if ! curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$temp/$archive" \
"https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive"; then
id=$(curl -fsSL --retry 3 --retry-all-errors -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/mcpp-community/mtest/releases/tags/v$MTEST_VERSION" \
| "$python" -c 'import json,sys; print(next(str(a["id"]) for a in json.load(sys.stdin)["assets"] if a["name"] == sys.argv[1]))' "$archive")
curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/octet-stream" -o "$temp/$archive" \
"https://api.github.com/repos/mcpp-community/mtest/releases/assets/$id"
fi
actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive")
[ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the workflow pins $MTEST_SHA256"; exit 1; }
mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest"
launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest"
[ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe"
fi
"$launcher" --version
"$launcher" --version | grep -qF "$MTEST_VERSION" || { echo "::error::the mtest at $launcher is not $MTEST_VERSION"; exit 1; }
echo "MTEST=$launcher" >> "$GITHUB_ENV"
79 changes: 77 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ env:
# fails with exit 2. This is the CI pin -- what this repository is
# tested against -- and not a floor: nothing here records a minimum
# engine for consumers.
MCPP_VERSION: 2026.11.5.1
MCPP_VERSION: 2026.11.9.1
XLINGS_VERSION: v2026.10.26.1
# The mtest engine the runs are reported with: the conformance suite's cases
# (tools/run-conformance.sh with MTEST set) and `mcpp test`'s store (target/mtest).
MTEST_VERSION: 0.5.2
MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6
XLINGS_NON_INTERACTIVE: '1'

jobs:
Expand Down Expand Up @@ -176,6 +180,8 @@ jobs:
esac
mcpp toolchain list

- uses: ./.github/actions/mtest

- name: Every interface, every kind of examination
run: |
# `optional` IS PART OF THE SET AND WAS NOT, WHICH MADE THE STEP'S
Expand Down Expand Up @@ -215,6 +221,11 @@ jobs:
done
test "$missing" -eq 0

- name: This package's test runs, kept for the report
if: always()
run: |
if [ -d target/mtest ]; then cp -R target/mtest "$RUNNER_TEMP/mcpp-test-runs"; fi

- name: The exported surface is complete and contains nothing else
run: |
rm -rf target && mcpp build
Expand Down Expand Up @@ -281,10 +292,13 @@ jobs:
for m in $(grep -oE '^# openkal\.[a-z]+' .spec/SURFACE.txt | cut -d. -f2); do
echo "import openkal.$m;"
done
echo 'import mcpplibs.mtest;'
echo 'const void *const surface[] = {'
while read -r n; do [ -n "$n" ] && echo " (const void *)&$n,"; done <<< "$list"
echo '};'
echo 'int main() { return surface[0] == 0; }'
echo 'mcpplibs::mtest::UTest conformance_declarations{"conformance_declarations", [] {'
echo ' mcpplibs::mtest::expect(surface[0] != nullptr) << "every name the specification lists is declared";'
echo '}};'
} > tests/conformance_declarations.cpp
mcpp test conformance_declarations 2>&1 | tee decl.log
grep -q '^conformance_declarations \.\.\. ok' decl.log
Expand Down Expand Up @@ -466,3 +480,64 @@ jobs:
fi
rm -f src/probe.cpp
echo "a dependence upon a C library is visible to the check"

# A FAILED RUN, EXPLAINED: through the engine the suite's own text is not in
# the log, so the failing cases are named here with what each observed.
- name: The failing cases, explained
if: failure()
run: |
[ -n "${MTEST:-}" ] && [ -d .spec/conformance/.mtest ] || exit 0
( cd .spec/conformance && "$MTEST" explain ) || true

# THE RUNS OF THIS JOB, REPORTED: the conformance suite's (one case per
# interface, its observations within it) and `mcpp test`'s (one case per
# test program), taken into one store, written as the job's summary and kept
# for the report over every job.
- name: The runs, reported
if: always()
run: |
[ -n "${MTEST:-}" ] || { echo "no mtest engine in this job"; exit 0; }
store="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")/mtest-store"
mkdir -p "$store"; here="$PWD"
for from in "$here/.spec/conformance/.mtest" "$RUNNER_TEMP/mcpp-test-runs"; do
if [ -f "$from/index.ndjson" ]; then ( cd "$store" && "$MTEST" store import --from "$from" ); fi
done
[ -f "$store/.mtest/index.ndjson" ] || { echo "no run was kept"; exit 0; }
( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@v4
if: always()
with:
name: mtest-${{ github.job }}-${{ strategy.job-index }}
path: ${{ runner.temp }}/mtest-store/.mtest
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7

# ---------------------------------------------------------------------------
# EVERY JOB'S RUNS AS ONE REPORT (`mtest ci report`, the workflow run's group).
report:
name: tests (report)
needs: conformance
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/mtest
- uses: actions/download-artifact@v4
with:
pattern: mtest-*
path: ${{ runner.temp }}/stores
- name: Every job's runs, reported together
shell: bash
run: |
set -euo pipefail
found=0
for store in "$RUNNER_TEMP"/stores/*/; do
[ -f "$store/index.ndjson" ] || continue
"$MTEST" store import --from "$store"
found=1
done
if [ "$found" = 0 ]; then echo "no job kept a run" >> "$GITHUB_STEP_SUMMARY"; exit 0; fi
"$MTEST" ci report --out "$RUNNER_TEMP/tests.md"
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,27 @@ for Linux, written on the kernel's own system-call interface.

```toml
[dependencies]
openkal = "0.24.0"
openkal = "0.25.0"

[target.'cfg(os = "linux")'.dependencies]
openkal-linux = "0.25.0"
openkal-linux = "0.26.0"
```

## What 0.26 adds (openkal 0.25)

**A second name for a file** (`kal_fs_hard_link`) is `linkat` with no flags, so a
final link receives the name itself. `KAL_FS_PROP_HARD_LINKS` is claimed on the
formats whose names the kernel keeps and that store several names for one node
(ext2/3/4, btrfs, xfs, f2fs, tmpfs, overlay, bcachefs, zfs, ntfs, hfsplus), and not
on FAT, exFAT, read-only formats or a format this implementation does not know. A
directory is refused with `kal_err_is_directory` before the call, because the
kernel's own answer (`EPERM`) names a permission; two volumes are refused by the
kernel with `EXDEV`, which is reported as `kal_err_not_supported`.

**The requests to end are counted.** The handler adds one to the stop word for each
request, and one where the count would wrap to zero, in a single compare-and-exchange,
so two handlers that interrupt one another each count.

## Observable process units

The process interface provides the OpenKal 0.24 declared-unit operations.
Expand Down
10 changes: 8 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.25.0"
version = "0.26.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 Down Expand Up @@ -67,7 +67,7 @@ provides-interfaces = [
]

[dependencies]
openkal = "0.24.0"
openkal = "0.25.0"

# The package contributes definitions and no modules. The interface it
# implements is declared by the specification package, which this package
Expand Down Expand Up @@ -113,3 +113,9 @@ default = []
# implementation, and the consumer that knows which arrangement holds is the
# one that declares it.
standalone = { defines = ["OKL_STANDALONE"] }

# The tests are written with the mtest library (mcpp-community/mtest): each file
# declares its cases, and a test that starts a copy of itself declares what the
# copy does as a `child'.
[dev-dependencies]
mcpplibs.mtest = "0.5.2"
35 changes: 31 additions & 4 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ kal_uintptr volume_props(int fd) {
case okl::fs_f2fs: case okl::fs_tmpfs: case okl::fs_overlay:
case okl::fs_bcachefs:
return local | KAL_FS_PROP_CASE_SENSITIVE
| KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS
| KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_HARD_LINKS
| KAL_FS_PROP_EXECUTABLE
| KAL_FS_PROP_RENAME_NO_REPLACE | KAL_FS_PROP_LOCAL_ENDPOINT;
case okl::fs_zfs:
return local | KAL_FS_PROP_CASE_SENSITIVE
| KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS
| KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_HARD_LINKS
| KAL_FS_PROP_EXECUTABLE | KAL_FS_PROP_LOCAL_ENDPOINT;

// Read-only formats: the nodes are there and none can be made, and a
Expand All @@ -263,10 +263,10 @@ kal_uintptr volume_props(int fd) {

// A case-insensitive namespace, with nodes that name others.
case okl::fs_ntfs: case okl::fs_ntfs3:
return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS;
return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_HARD_LINKS;
// The same, on a format that stores a mode.
case okl::fs_hfsplus:
return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS
return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_HARD_LINKS
| KAL_FS_PROP_EXECUTABLE;

default:
Expand Down Expand Up @@ -1079,6 +1079,33 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len,
return kal_ok;
}

// A SECOND NAME FOR A FILE (0.25) is `linkat' with no flags, which gives the
// node the name denotes another name and does not follow a final link. A
// directory is refused before the call, because the kernel refuses it with
// EPERM, which reads as a permission. Two volumes are refused by the kernel with
// EXDEV, which is reported as kal_err_not_supported: an operation the pair
// cannot perform.
int kal_fs_hard_link(kal_dir from, const char* a, kal_uintptr alen,
kal_dir to, const char* b, kal_uintptr blen) {
const int f = okl::unpack(from.h), t2 = okl::unpack(to.h);
if (f < 0 || t2 < 0 || !okl::acceptable(a, alen) || !okl::acceptable(b, blen)) return kal_err_invalid;
if ((volume_props(t2) & KAL_FS_PROP_HARD_LINKS) == 0) return kal_err_not_supported;
okl::terminated ta(a, alen), tb(b, blen);
if (!ta.ok || !tb.ok) return kal_err_invalid;
if (refused_write(t2)) return kal_err_permission;
okl::kstat st{};
const okl_long s = okl::sys(okl::nr_newfstatat, f, reinterpret_cast<okl_long>(ta.buf),
reinterpret_cast<okl_long>(&st), okl::at_symlink_nofollow);
if (okl::failed(s)) return okl::translate(s);
if ((st.mode & okl::s_ifmt) == okl::s_ifdir) return kal_err_is_directory;
const okl_long r = okl::sys(okl::nr_linkat, f, reinterpret_cast<okl_long>(ta.buf),
t2, reinterpret_cast<okl_long>(tb.buf), 0);
if (r == -okl::e_xdev) return kal_err_not_supported;
if (okl::failed(r)) return okl::translate(r);
wrote(t2, b, blen);
return kal_ok;
}

// THE NAME WITH EVERY LINK FOLLOWED (0.23): the node opened for its path alone
// (O_PATH, which needs no permission upon it and follows every link), and the
// kernel's record of that descriptor's name read, as this system's own C
Expand Down
12 changes: 11 additions & 1 deletion src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,17 @@ void stop_handler(int signo) {
int none = 0;
__atomic_compare_exchange_n(&g_stop_signal, &none, signo, false,
__ATOMIC_RELAXED, __ATOMIC_RELAXED);
__atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE);
// EACH REQUEST IS COUNTED (openkal 0.25), and the count skips zero when it
// wraps, so the word is never zero again once a request has arrived. One
// exchange both counts and skips zero, so handlers of two signals that
// interrupt one another each count, at the wrap as everywhere else; an add
// followed by a fix-up could lose the second count between the two.
kal_u32 seen = __atomic_load_n(&g_stop_word, __ATOMIC_RELAXED), next;
do {
next = seen + 1u;
if (next == 0) next = 1u;
} while (!__atomic_compare_exchange_n(&g_stop_word, &seen, next, false,
__ATOMIC_RELEASE, __ATOMIC_RELAXED));
// PRIVATE, AS `kal_task_wait' WAITS (0.18.2). The kernel keys a private
// futex apart from a shared one at the same address, so a shared wake
// reached no context waiting through `kal_task_wait' with no bound, and a
Expand Down
4 changes: 2 additions & 2 deletions src/sys.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ enum : okl_long {
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_close_range = 436, nr_prlimit64 = 302,
nr_dup2 = 33, nr_utimensat = 280, nr_symlinkat = 266, nr_fstatfs = 138,
nr_dup2 = 33, nr_utimensat = 280, nr_symlinkat = 266, nr_linkat = 265, nr_fstatfs = 138,
nr_getrandom = 318, nr_sched_setscheduler = 144,
// openkal 0.15: the region a context stands on is measured by asking the
// kernel which pages are mapped. The floor comes from the limit the kernel
Expand Down Expand Up @@ -212,7 +212,7 @@ enum : okl_long {
nr_dup3 = 24, nr_execveat = 281,
nr_close_range = 436, nr_prlimit64 = 261, nr_dup2 = -1, nr_fcntl = 25,
nr_prctl = 167, nr_sched_getaffinity = 123, nr_getppid = 173,
nr_arch_prctl = -1, nr_utimensat = 88, nr_symlinkat = 36, nr_fstatfs = 44,
nr_arch_prctl = -1, nr_utimensat = 88, nr_symlinkat = 36, nr_linkat = 37, nr_fstatfs = 44,
nr_getrandom = 278, nr_sched_setscheduler = 119,
// openkal 0.15: the region a context stands on is measured by asking the
// kernel which pages are mapped. The floor comes from the limit the kernel
Expand Down
14 changes: 6 additions & 8 deletions tests/conformance_additions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import openkal.env;
import openkal.abort;
import openkal.memory;
import openkal.process;
import mcpplibs.mtest;

namespace {
using namespace mcpplibs::mtest;

int failures = 0;
namespace {

void check(bool held, const char* what) {
if (!held) { std::printf("FAIL: %s\n", what); ++failures; }
}
void check(bool held, const char* what) { expect(held) << what; }

kal_dir here() { return kal::fs::working(); }

Expand All @@ -51,7 +50,7 @@ long get(const char* name, char* buf, kal_uintptr cap) {

} // namespace

int main() {
UTest conformance_additions_test{"conformance_additions", [] {
const char* name = "okl-additions.tmp";
const kal_uintptr n = std::strlen(name);
char buf[256];
Expand Down Expand Up @@ -213,5 +212,4 @@ int main() {
}

std::printf("openkal-linux: the operations version 0.5 added\n");
return failures == 0 ? 0 : 1;
}
}};
Loading
Loading