From a5a9a8671e0e976502077f3c82ecace6d7cce36d Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 03:31:44 +0900 Subject: [PATCH 1/6] 0.26.0: a second name for a file, and the requests to end counted (openkal 0.25) - kal_fs_hard_link: linkat with no flags; KAL_FS_PROP_HARD_LINKS on the formats that store several names for one node; a directory is kal_err_is_directory and two volumes kal_err_not_supported, answered before the kernel is asked - the stop word counts each request and skips zero on wrap - openkal follows the specification's branch feat/0.25.0 until 0.25.0 is released --- README.md | 18 ++++++++++++++++-- mcpp.toml | 4 ++-- src/fs.cpp | 35 +++++++++++++++++++++++++++++++---- src/process.cpp | 7 ++++++- src/sys.h | 4 ++-- 5 files changed, 57 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dbedd71..91edab4 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,26 @@ 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` and two volumes with +`kal_err_not_supported` before the kernel is asked, because its own answers +(`EPERM`, `EXDEV`) name other conditions. + +**The requests to end are counted.** The handler adds one to the stop word for each +request and writes one where the count wraps to zero. + ## Observable process units The process interface provides the OpenKal 0.24 declared-unit operations. diff --git a/mcpp.toml b/mcpp.toml index dffa255..f82ce70 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" @@ -67,7 +67,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.24.0" +openkal = { git = "https://github.com/mcpplibs/openkal.git", branch = "feat/0.25.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/fs.cpp b/src/fs.cpp index 51839c6..f9cdc99 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -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 @@ -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: @@ -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. Two +// conditions are answered before the call, because the kernel's own answers +// name a different condition: a directory is refused with EPERM, which reads as +// a permission, and two volumes with EXDEV, which this interface states as 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(ta.buf), + reinterpret_cast(&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(ta.buf), + t2, reinterpret_cast(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 diff --git a/src/process.cpp b/src/process.cpp index c051568..7aa3c04 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1260,7 +1260,12 @@ 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. The + // add is atomic, so handlers of two signals that interrupt one another each + // count; the fix-up after a wrap only ever writes one. + if (__atomic_add_fetch(&g_stop_word, 1u, __ATOMIC_RELEASE) == 0) + __atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE); // 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 diff --git a/src/sys.h b/src/sys.h index dca81ae..8ba4b80 100644 --- a/src/sys.h +++ b/src/sys.h @@ -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 @@ -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 From 4f3ae2e3b690b2af785e87a29d6ec04b8347ffd8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 03:59:40 +0900 Subject: [PATCH 2/6] ci: the runs are kept and reported through mtest - the conformance suite runs through the pinned mtest engine (MTEST, set by .github/actions/mtest): one case per interface, its observations within it - mcpp 2026.11.9.1: `mcpp test` keeps this package's runs in the mtest store (target/mtest), one case per test program - each job's runs in its summary and as an artifact; one report over every job The test programs stay programs of their own (protocol level 0): the mtest library reaches mcpplibs.os, which depends on openkal and on this implementation, so a dev-dependency on it would close a cycle through the package under test. --- .github/actions/mtest/action.yml | 69 +++++++++++++++++++++++++++++ .github/workflows/ci.yml | 74 +++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 .github/actions/mtest/action.yml diff --git a/.github/actions/mtest/action.yml b/.github/actions/mtest/action.yml new file mode 100644 index 0000000..549468c --- /dev/null +++ b/.github/actions/mtest/action.yml @@ -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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31e356a..050843c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 @@ -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 @@ -466,3 +477,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" From 05f01b5476a89e6abf2bf1662b81b8f135c0f365 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 04:39:12 +0900 Subject: [PATCH 3/6] Tests: written with the mtest library Every test under tests/ declares its cases as mtest UTest objects, with the same assertions as before. A test that starts a copy of itself declares what the copy does as a child and starts it with child_command. The generated declarations test is a UTest too. mcpplibs.mtest 0.5.2 is a dev-dependency. --- .github/workflows/ci.yml | 5 +- mcpp.toml | 6 ++ tests/conformance_additions.cpp | 14 ++- tests/conformance_env_time.cpp | 19 ++-- tests/conformance_fs.cpp | 19 ++-- tests/conformance_memory.cpp | 18 ++-- tests/conformance_process_task.cpp | 19 ++-- tests/conformance_spawn.cpp | 47 ++++----- tests/conformance_stack_size.cpp | 20 ++-- tests/conformance_stream.cpp | 22 ++--- tests/conformance_task_tls.cpp | 49 ++++------ tests/conformance_terminal.cpp | 22 ++--- tests/conformance_v08.cpp | 16 ++-- tests/identity_in_a_pid_namespace.cpp | 29 +++--- tests/openkal_0_20.cpp | 19 ++-- tests/openkal_0_21.cpp | 19 ++-- tests/openkal_0_22.cpp | 55 ++++++----- tests/openkal_0_23.cpp | 132 ++++++++++++++------------ tests/script_sees_its_name.cpp | 25 ++--- tests/search_only_base.cpp | 15 ++- tests/sigpipe_not_passed_on.cpp | 110 ++++++++++++--------- tests/spawn_without_execveat.cpp | 18 ++-- tests/stop_word_take_over.cpp | 125 ++++++++++++------------ 23 files changed, 387 insertions(+), 436 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 050843c..66634c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -292,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 diff --git a/mcpp.toml b/mcpp.toml index f82ce70..f2ccf19 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" diff --git a/tests/conformance_additions.cpp b/tests/conformance_additions.cpp index e5aef1e..52437f5 100644 --- a/tests/conformance_additions.cpp +++ b/tests/conformance_additions.cpp @@ -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(); } @@ -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]; @@ -213,5 +212,4 @@ int main() { } std::printf("openkal-linux: the operations version 0.5 added\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_env_time.cpp b/tests/conformance_env_time.cpp index e8ddc32..dc6f1fd 100644 --- a/tests/conformance_env_time.cpp +++ b/tests/conformance_env_time.cpp @@ -2,21 +2,15 @@ import openkal.env; import openkal.time; import openkal.stream; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } } -int main() { +UTest conformance_env_time_test{"conformance_env_time", [] { // 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"); @@ -53,5 +47,4 @@ int main() { const char ok[] = "openkal-linux: env and time conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index a09a7c8..673c564 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -6,21 +6,15 @@ import openkal.fs; import openkal.stream; import openkal.task; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } } -int main() { +UTest conformance_fs_test{"conformance_fs", [] { check(kal::fs::preopen_count() >= 1, "the environment supplies at least one directory"); auto root = kal::fs::working(); check(root.h != 0, "the working directory is the first entry"); @@ -204,5 +198,4 @@ int main() { const char ok[] = "openkal-linux: file system conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_memory.cpp b/tests/conformance_memory.cpp index 9aec6eb..78f5578 100644 --- a/tests/conformance_memory.cpp +++ b/tests/conformance_memory.cpp @@ -1,22 +1,22 @@ // Conformance: openkal.memory. import openkal.memory; import openkal.stream; +import mcpplibs.mtest; -int main() { - void* p = kal::alloc(128, 16); - if (p == nullptr) return 1; - if ((reinterpret_cast(p) % 16) != 0) return 1; +using namespace mcpplibs::mtest; +UTest conformance_memory_test{"conformance_memory", [] { + void* p = kal::alloc(128, 16); + if (p == nullptr) { fail("p == nullptr"); return; } + if ((reinterpret_cast(p) % 16) != 0) { fail("(reinterpret_cast(p) % 16) != 0"); return; } auto* b = static_cast(p); for (int i = 0; i < 128; ++i) b[i] = static_cast(i); - for (int i = 0; i < 128; ++i) if (b[i] != static_cast(i)) return 1; + for (int i = 0; i < 128; ++i) if (b[i] != static_cast(i)) { fail("the bytes written read back"); return; } kal::free(p, 128, 16); // A zero-sized request yields a null pointer rather than a region that // cannot be used. - if (kal::alloc(0, 8) != nullptr) return 1; - + if (kal::alloc(0, 8) != nullptr) { fail("kal::alloc(0, 8) != nullptr"); return; } const char ok[] = "openkal-linux: memory conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return 0; -} +}}; diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index f895055..cd1ea4e 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -3,18 +3,12 @@ import openkal.process; import openkal.task; import openkal.fs; import openkal.stream; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } // Shared between contexts. The word is what openkal.task suspends upon, and it // is ordinary memory: the interface adds no object of its own. @@ -31,7 +25,7 @@ void worker(void* arg) { } } -int main() { +UTest conformance_process_task_test{"conformance_process_task", [] { // --- openkal.process ----------------------------------------------------- // // The program to start is reached through a directory the environment @@ -190,5 +184,4 @@ int main() { const char ok[] = "openkal-linux: process and task conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_spawn.cpp b/tests/conformance_spawn.cpp index 3260a2e..9f0e2cb 100644 --- a/tests/conformance_spawn.cpp +++ b/tests/conformance_spawn.cpp @@ -5,24 +5,25 @@ // the claim is about the started image and not about the caller. Two programs // are used. `/bin/sh' is an ordinary executable that needs nothing to survive // the start, so any descriptor above 2 it holds was conveyed by the start. This -// test itself, started again with `--grant-child', reads its preopens through +// test itself, started again as the child `grant', reads its preopens through // `kal_fs_preopen', which is how a granted directory is defined to arrive. #include #include #include +#include +#include #include import openkal.fs; import openkal.stream; import openkal.types; 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; } const char kMarker[] = "okl-grant-marker"; @@ -47,23 +48,25 @@ bool write_marker(kal_dir d, const char* text) { // --- the started side -------------------------------------------------------- // -// argv: --grant-child { } +// arguments: { } // is the content of the marker file in that directory, or `@' // for a directory entry that must exist there. The status names the first // disagreement, so that a failure in the caller says which one it was. -int grant_child(int argc, char** argv) { +int grant_child(const std::vector& argv) { const kal_uintptr n = kal_fs_preopen_count(); - if (std::strcmp(argv[2], "ambient") == 0) { + if (argv.empty()) return 90; + if (argv[0] == "ambient") { kal_dir d{}; char nm[8]; kal_uintptr l = 0; if (n != 2) return 10; if (kal_fs_preopen(1, &d, nm, sizeof nm, &l) != kal_ok || l != 1 || nm[0] != '/') return 11; return 0; } - const kal_uintptr want = static_cast(std::atoi(argv[2])); + const kal_uintptr want = static_cast(std::atoi(argv[0].c_str())); + if (argv.size() != 1 + 2 * want) return 91; if (n != want) return 20; for (kal_uintptr i = 0; i < want; ++i) { - const char* name = argv[3 + 2 * i]; - const char* expect = argv[4 + 2 * i]; + const char* name = argv[1 + 2 * i].c_str(); + const char* expect = argv[2 + 2 * i].c_str(); kal_dir d{}; char nm[256]; kal_uintptr l = 0; if (kal_fs_preopen(i, &d, nm, sizeof nm, &l) != kal_ok) return 30 + static_cast(i); if (l != std::strlen(name) || std::memcmp(nm, name, l) != 0) return 40 + static_cast(i); @@ -85,6 +88,8 @@ int grant_child(int argc, char** argv) { return 0; } +child grant{"grant", [](std::vector args) { return grant_child(args); }}; + // --- the starting side ------------------------------------------------------- char g_self[1024]; @@ -94,15 +99,16 @@ char g_self[1024]; int run_child(const kal_preopen* grants, kal_uintptr count, const char* const* args, kal_uintptr nargs, const char** envp = nullptr, kal_uintptr envc = 0) { - const char* argv[16] = { "conformance_spawn", "--grant-child" }; - kal_uintptr lens[16] = { 17, 13 }; - for (kal_uintptr i = 0; i < nargs; ++i) { argv[2 + i] = args[i]; lens[2 + i] = std::strlen(args[i]); } + const std::vector command = child_command("grant", std::vector(args, args + nargs)); + std::vector argv; + std::vector lens; + for (const auto& x : command) { argv.push_back(x.c_str()); lens.push_back(x.size()); } kal_uintptr elens[4] = {}; for (kal_uintptr i = 0; i < envc; ++i) elens[i] = std::strlen(envp[i]); const kal_spawn how{ root(), working(), nullptr, grants, count, 0 }; kal_process p{}; const char* rel = g_self + 1; - if (kal_process_spawn(&how, rel, std::strlen(rel), argv, lens, 2 + nargs, + if (kal_process_spawn(&how, rel, std::strlen(rel), argv.data(), lens.data(), argv.size(), envp, elens, envc, nullptr, &p) != kal_ok) return -1; int status = -1, terminated = -1; @@ -113,9 +119,7 @@ int run_child(const kal_preopen* grants, kal_uintptr count, } // namespace -int main(int argc, char** argv) { - if (argc > 2 && std::strcmp(argv[1], "--grant-child") == 0) return grant_child(argc, argv); - +UTest conformance_spawn_test{"conformance_spawn", [] { const ssize_t sl = readlink("/proc/self/exe", g_self, sizeof g_self - 1); check(sl > 1 && g_self[0] == '/', "the test can name its own program"); g_self[sl > 0 ? sl : 0] = '\0'; @@ -220,7 +224,4 @@ int main(int argc, char** argv) { kal_fs_close_dir(b); kal_fs_remove(working(), da, std::strlen(da)); kal_fs_remove(working(), db, std::strlen(db)); - - std::printf("openkal-linux: what a started program receives\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_stack_size.cpp b/tests/conformance_stack_size.cpp index 327c00c..ad62f75 100644 --- a/tests/conformance_stack_size.cpp +++ b/tests/conformance_stack_size.cpp @@ -7,18 +7,12 @@ import openkal.types; import openkal.task; import openkal.stream; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } long recurse(long n); long (*volatile g_recurse)(long) = recurse; @@ -40,7 +34,7 @@ void room(void* p) { } } -int main() { +UTest conformance_stack_size_test{"conformance_stack_size", [] { const kal_uintptr asks[] = { 1u << 20, 16u << 20, 64u << 20 }; for (kal_uintptr ask : asks) { probe q{ ask, 0, 0 }; @@ -54,6 +48,4 @@ int main() { kal_task never{}; check(kal_task_start_sized(room, nullptr, ~kal_uintptr{0} - 4095, &never) == kal_err_no_memory, "a stack larger than the address space is reported as exhaustion"); - if (failures == 0) kal::write(kal::out(), "openkal-linux: a least stack size is room\n", 42); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_stream.cpp b/tests/conformance_stream.cpp index 6e5beff..5faee74 100644 --- a/tests/conformance_stream.cpp +++ b/tests/conformance_stream.cpp @@ -6,25 +6,17 @@ // omitted, and it is the half that keeps a capability claim from diverging from // the code behind it. import openkal.stream; +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool ok, const char* what) { - if (!ok) { - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n] != '\0') ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); - } -} +void check(bool ok, const char* what) { expect(ok) << what; } } // namespace -int main() { +UTest conformance_stream_test{"conformance_stream", [] { // The standard streams are distinct and usable. check(kal::in().h != kal::out().h, "stdin and stdout are distinct"); check(kal::out().h != kal::err().h, "stdout and stderr are distinct"); @@ -51,6 +43,4 @@ int main() { // program, by comparing the exported names of the implementation against // the specification's list. Version 0.2 defines no optional operation, so // there is nothing here for a compile-time assertion to check. - - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_task_tls.cpp b/tests/conformance_task_tls.cpp index 622e470..47331c0 100644 --- a/tests/conformance_task_tls.cpp +++ b/tests/conformance_task_tls.cpp @@ -23,6 +23,9 @@ import openkal.types; import openkal.task; import openkal.stream; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { @@ -68,32 +71,24 @@ void examine(void*) { } // namespace -int main() { - int failures = 0; - +UTest conformance_task_tls_test{"conformance_task_tls", [] { for (int i = 0; i < kSize; ++i) g_block[i] = kStarter; kal_task t{}; - if (kal_task_start(examine, nullptr, &t) != kal_ok) { - std::printf("FAIL: a context was not started\n"); - return 1; - } - if (kal_task_join(t) != kal_ok) { - std::printf("FAIL: the context was not awaited\n"); - return 1; - } - - if (g_ran != 1) { std::printf("FAIL: the context did not run\n"); ++failures; } - if (g_image != 1) { std::printf("FAIL: the declared value was not in the image\n"); ++failures; } - if (g_fresh != 1) { std::printf("FAIL: the block did not begin as the image says\n"); ++failures; } - if (g_kept != 1) { std::printf("FAIL: the context's writes did not survive it\n"); ++failures; } - if (g_isolated != 1) { std::printf("FAIL: the context's writes disturbed its own image\n"); ++failures; } + if (kal_task_start(examine, nullptr, &t) != kal_ok) { fail("a context was not started"); return; } + if (kal_task_join(t) != kal_ok) { fail("the context was not awaited"); return; } + + expect(eq(g_ran, 1)) << "the context ran"; + expect(eq(g_image, 1)) << "the declared value was in the image"; + expect(eq(g_fresh, 1)) << "the block began as the image says"; + expect(eq(g_kept, 1)) << "the context's writes survived it"; + expect(eq(g_isolated, 1)) << "the context's writes did not disturb its own image"; // The starter's instance is untouched, which is the other half of // "distinct": one instance per context and not one shared. int intact = 1; for (int i = 0; i < kSize; ++i) if (g_block[i] != kStarter) { intact = 0; break; } - if (intact != 1) { std::printf("FAIL: the starting context's instance was written\n"); ++failures; } + expect(eq(intact, 1)) << "the starting context's instance was not written"; // A second context starts from the image again, which is what makes the // storage per context rather than per process. @@ -103,17 +98,7 @@ int main() { g_kept = 0; g_isolated = 0; kal_task u{}; - if (kal_task_start(examine, nullptr, &u) == kal_ok && kal_task_join(u) == kal_ok - && g_image == 1 && g_fresh == 1 && g_kept == 1 && g_isolated == 1) { - // held - } else { - std::printf("FAIL: a second context did not see its own instance\n"); - ++failures; - } - - if (failures == 0) { - const char ok[] = "openkal-linux: a started context's thread-local storage\n"; - kal::write(kal::out(), ok, sizeof(ok) - 1); - } - return failures == 0 ? 0 : 1; -} + expect(kal_task_start(examine, nullptr, &u) == kal_ok && kal_task_join(u) == kal_ok + && g_image == 1 && g_fresh == 1 && g_kept == 1 && g_isolated == 1) + << "a second context sees its own instance"; +}}; diff --git a/tests/conformance_terminal.cpp b/tests/conformance_terminal.cpp index e3b65f2..df1c92b 100644 --- a/tests/conformance_terminal.cpp +++ b/tests/conformance_terminal.cpp @@ -14,18 +14,12 @@ import openkal.types; import openkal.stream; import openkal.terminal; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } constexpr kal_uintptr kRaw = kal::terminal::pass_control.bits; // line assembly and echo off constexpr tcflag_t kAlters = ISTRIP | INLCR | IGNCR | ICRNL; @@ -43,11 +37,11 @@ int deliver(int master, kal_stream s, const char* in, int n, unsigned char* out) } } -int main() { +UTest conformance_terminal_test{"conformance_terminal", [] { const int master = posix_openpt(O_RDWR | O_NOCTTY); if (master < 0 || grantpt(master) != 0 || unlockpt(master) != 0) { kal::write(kal::err(), "no pseudo terminal; not observed\n", 33); - return 0; + return; } const int slave = open(ptsname(master), O_RDWR | O_NOCTTY); check(slave >= 0, "the pseudo terminal opens"); @@ -101,6 +95,4 @@ int main() { "with line assembly on, the carriage return is part of the line"); close(slave); close(master); - if (failures == 0) kal::write(kal::out(), "openkal-linux: the terminal's alteration of input\n", 51); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_v08.cpp b/tests/conformance_v08.cpp index c5bc38e..558d305 100644 --- a/tests/conformance_v08.cpp +++ b/tests/conformance_v08.cpp @@ -20,14 +20,13 @@ import openkal.space; import openkal.timeout; import openkal.process; import openkal.abort; +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_endpoint loopback(kal_u32 port) { kal_endpoint ep{}; @@ -343,7 +342,7 @@ void process_additions_section() { } // namespace -int main() { +UTest conformance_v08_test{"conformance_v08", [] { // FIRST, AND THE ORDER IS LOAD-BEARING. The comment above this section // records why: it examines a defect that corrects itself for any descriptor // index at which an owned handle has already been released, so running it @@ -356,7 +355,4 @@ int main() { datagram_section(); space_section(); timeout_section(); - - if (failures == 0) std::printf("openkal 0.8 interfaces: every observation held\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/identity_in_a_pid_namespace.cpp b/tests/identity_in_a_pid_namespace.cpp index 364b7e5..a8f8b29 100644 --- a/tests/identity_in_a_pid_namespace.cpp +++ b/tests/identity_in_a_pid_namespace.cpp @@ -14,6 +14,9 @@ #include import openkal.types; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { struct identity { kal_u64 id = 0, started = 0; int ended = 0; }; @@ -41,11 +44,11 @@ bool find(kal_u64 id, identity& out) { } } -int main() { +UTest identity_in_a_pid_namespace_test{"identity_in_a_pid_namespace", [] { identity outside{}; - if (!self(outside)) { std::printf("FAIL: this program names itself\n"); return 1; } + if (!self(outside)) { fail("this program names itself"); return; } int report[2], release[2]; - if (::pipe(report) != 0 || ::pipe(release) != 0) return 1; + if (::pipe(report) != 0 || ::pipe(release) != 0) { fail("two pipes are made"); return; } const pid_t helper = ::fork(); if (helper == 0) { if (::unshare(CLONE_NEWUSER | CLONE_NEWPID) != 0) { @@ -73,21 +76,15 @@ int main() { } identity inner{}; (void)!::read(report[0], &inner, sizeof inner); - int failures = 0; - if (inner.id == 0) { - std::printf("openkal-linux: no pid namespace can be made here; nothing to observe\n"); - } else { + if (inner.id != 0) { identity seen{}; - if (!find(inner.id, seen) || seen.ended || seen.started != inner.started) { - std::printf("FAIL: outside, the program inside is not found by the number it named itself by\n"); - ++failures; - } + expect(find(inner.id, seen) && !seen.ended && seen.started == inner.started) + << "outside, the program inside is found by the number it named itself by"; } (void)!::write(release[1], "x", 1); int st = 0; ::waitpid(helper, &st, 0); - if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) ++failures; - if (failures == 0 && inner.id != 0) - std::printf("openkal-linux: a program in a pid namespace is named as /proc names it, both ways\n"); - return failures; -} + expect(WIFEXITED(st) && WEXITSTATUS(st) == 0) + << "inside, the program names itself and finds the program outside (the helper's status)"; + if (inner.id == 0) skip("no pid namespace can be made here; nothing to observe"); +}}; diff --git a/tests/openkal_0_20.cpp b/tests/openkal_0_20.cpp index 2c32f24..1e956ae 100644 --- a/tests/openkal_0_20.cpp +++ b/tests/openkal_0_20.cpp @@ -14,18 +14,12 @@ import openkal.stream; import openkal.task; import openkal.time; import openkal.types; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } constexpr kal_uintptr kSize = 1u << 16; constexpr char kName[] = "openkal-0-20-at-offsets.tmp"; @@ -60,7 +54,7 @@ void read_clock(void* arg) { } } // namespace -int main() { +UTest openkal_0_20_test{"openkal_0_20", [] { const kal_dir here = kal::fs::working(); { kal_file f{}; @@ -110,5 +104,4 @@ int main() { check(b > a && b - a >= 19u * 1000u * 1000u && b - a < 1000u * 1000u * 1000u, "the wall clock advances with a sleep, and by about as much"); } - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/openkal_0_21.cpp b/tests/openkal_0_21.cpp index e6828ea..82722d3 100644 --- a/tests/openkal_0_21.cpp +++ b/tests/openkal_0_21.cpp @@ -10,18 +10,12 @@ import openkal.memory; import openkal.process; import openkal.stream; import openkal.types; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } kal_u64 resident() { kal_process self{}; @@ -34,7 +28,7 @@ kal_u64 resident() { } } // namespace -int main() { +UTest openkal_0_21_test{"openkal_0_21", [] { { void* blocks[12] = {}; const kal_u64 before = resident(); @@ -58,5 +52,4 @@ int main() { kal_free(p, 100, a); } } - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/openkal_0_22.cpp b/tests/openkal_0_22.cpp index 19cd75e..91f3f35 100644 --- a/tests/openkal_0_22.cpp +++ b/tests/openkal_0_22.cpp @@ -7,6 +7,8 @@ // afterwards. #include #include +#include +#include #include #include #include @@ -14,12 +16,12 @@ import openkal.types; import openkal.env; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -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 root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { @@ -38,28 +40,33 @@ int descriptors() { char g_self[4096]; } // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--null") == 0) { - struct stat in{}, err{}; - if (::fstat(0, &in) != 0 || !S_ISCHR(in.st_mode)) return 41; - if (::fstat(2, &err) != 0 || !S_ISCHR(err.st_mode)) return 42; - if (in.st_rdev != err.st_rdev) return 43; - char b[8]; - if (::read(0, b, sizeof b) != 0) return 44; - if (::write(2, "discarded", 9) != 9) return 45; - return 0; - } +// THE STARTED COPY (`null'): it finds one character device at standard input +// and standard error, reads the end of input there and writes to it. +child null_device{"null", [](std::vector) { + struct stat in{}, err{}; + if (::fstat(0, &in) != 0 || !S_ISCHR(in.st_mode)) return 41; + if (::fstat(2, &err) != 0 || !S_ISCHR(err.st_mode)) return 42; + if (in.st_rdev != err.st_rdev) return 43; + char b[8]; + if (::read(0, b, sizeof b) != 0) return 44; + if (::write(2, "discarded", 9) != 9) return 45; + return 0; +}}; + +UTest openkal_0_22_test{"openkal_0_22", [] { const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); - if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } + if (n <= 0 || g_self[0] != '/') { fail("the program's own image is named"); return; } g_self[n] = 0; const kal_dir r = root(); - const char* args[] = { g_self, "--null" }; - const kal_uintptr lens[] = { std::strlen(g_self), 6 }; + const std::vector command = child_command("null"); + std::vector args; + std::vector lens; + for (const auto& a : command) { args.push_back(a.c_str()); lens.push_back(a.size()); } const kal_spawn how{ r, r, nullptr, nullptr, 0, (kal::process::null_in_flag | kal::process::null_err_flag).bits }; const int before = descriptors(); kal_process p{}; - const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args, lens, 2, + const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args.data(), lens.data(), args.size(), nullptr, nullptr, 0, nullptr, &p); check(e == kal_ok, "a program is started with the null device at two positions"); check(descriptors() == before, "and the caller holds no descriptor for the device"); @@ -68,9 +75,7 @@ int main(int argc, char** argv) { kal_process_wait(p, &status, &terminated); kal_process_close(p); } - check(terminated == 0 && status == 0, - "the started program finds one character device there, reads its end and writes to it"); - if (status > 0) std::printf(" the started program reported %d\n", status); - if (failures == 0) std::printf("openkal_0_22: ok\n"); - return failures == 0 ? 0 : 1; -} + expect(terminated == 0 && status == 0) + << "the started program finds one character device there, reads its end and writes to it (reported " + << status << ")"; +}}; diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp index 74ff112..4edd00b 100644 --- a/tests/openkal_0_23.cpp +++ b/tests/openkal_0_23.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,12 +21,12 @@ import openkal.env; import openkal.fs; import openkal.process; import openkal.abort; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -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 root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { @@ -40,12 +42,16 @@ char g_dir[] = "/tmp/okl-0.24-XXXXXX"; int start(const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uintptr flags, const char* operand = nullptr) { const kal_dir r = root(); - const char* args[] = { g_self, errand, operand }; - const kal_uintptr lens[] = { std::strlen(g_self), std::strlen(errand), operand ? std::strlen(operand) : 0 }; + std::vector operands; + if (operand) operands.emplace_back(operand); + const std::vector command = child_command(errand, operands); + std::vector args; + std::vector lens; + for (const auto& a : command) { args.push_back(a.c_str()); lens.push_back(a.size()); } const kal_spawn how{ r, r, nullptr, grants, n, flags }; kal_process p{}; - if (kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args, lens, operand ? 3 : 2, nullptr, nullptr, 0, - nullptr, &p) != kal_ok) + if (kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args.data(), lens.data(), args.size(), nullptr, + nullptr, 0, nullptr, &p) != kal_ok) return -1; int status = -1, terminated = 0; kal_process_wait(p, &status, &terminated); @@ -54,49 +60,55 @@ int start(const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uint } } // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--read") == 0) { - // The first preopen is the recorded grant: read a file, ask for an - // absent one, and end through kal_exit so the record is ended. - kal_dir d{}; kal_uintptr l = 0; - if (kal_fs_preopen(0, &d, nullptr, 0, &l) != kal_ok) kal_exit(51); - kal_file f{}; - if (kal_fs_open(d, "present.txt", 11, kal::fs::open::read.bits, &f) != kal_ok) kal_exit(52); - kal_fs_close_file(f); - kal_node_info i{}; i.self_size = sizeof i; - kal_fs_info(d, "absent.txt", 10, 0, kal::fs::field::kind, &i); - kal_exit(0); - } - if (argc > 1 && std::strcmp(argv[1], "--write-raw") == 0) { - // Past openkal: the kernel itself is asked to create a file in the - // read-only grant, and in a directory not granted at all. - const int a = static_cast(::syscall(SYS_openat, AT_FDCWD, "/tmp/okl-0.24-raw.tmp", O_CREAT | O_WRONLY, 0600)); - if (a >= 0) { ::unlink("/tmp/okl-0.24-raw.tmp"); return 61; } - const int b = static_cast(::syscall(SYS_openat, AT_FDCWD, "/etc/hostname", O_RDONLY)); - if (b >= 0) return 62; - return 60; - } - if (argc > 1 && std::strcmp(argv[1], "--thp") == 0) - return static_cast(::prctl(PR_GET_THP_DISABLE, 0, 0, 0, 0)) == 1 ? 70 : 71; - if (argc == 3 && (std::strcmp(argv[1], "--truncate-raw") == 0 || - std::strcmp(argv[1], "--open-truncate-raw") == 0)) { - // Prove the existing file is readable before asking the kernel to - // truncate it. Missing files and unreachable grants are not proof. - const int readable = static_cast(::syscall(SYS_openat, AT_FDCWD, argv[2], O_RDONLY)); - if (readable < 0) return 66; - ::close(readable); - errno = 0; - const long result = std::strcmp(argv[1], "--truncate-raw") == 0 - ? ::syscall(SYS_truncate, argv[2], 0) - : ::syscall(SYS_openat, AT_FDCWD, argv[2], O_RDONLY | O_TRUNC); - const int why = errno; - if (result >= 0) { - if (std::strcmp(argv[1], "--open-truncate-raw") == 0) ::close(static_cast(result)); - return 64; - } - return why == EACCES ? 63 : 65; +// The first preopen is the recorded grant: read a file, ask for an absent one, +// and end through kal_exit so the record is ended. +child read_granted{"read", [](std::vector) -> int { + kal_dir d{}; kal_uintptr l = 0; + if (kal_fs_preopen(0, &d, nullptr, 0, &l) != kal_ok) kal_exit(51); + kal_file f{}; + if (kal_fs_open(d, "present.txt", 11, kal::fs::open::read.bits, &f) != kal_ok) kal_exit(52); + kal_fs_close_file(f); + kal_node_info i{}; i.self_size = sizeof i; + kal_fs_info(d, "absent.txt", 10, 0, kal::fs::field::kind, &i); + kal_exit(0); +}}; + +// Past openkal: the kernel itself is asked to create a file in the read-only +// grant, and in a directory not granted at all. +child write_raw{"write-raw", [](std::vector) { + const int a = static_cast(::syscall(SYS_openat, AT_FDCWD, "/tmp/okl-0.24-raw.tmp", O_CREAT | O_WRONLY, 0600)); + if (a >= 0) { ::unlink("/tmp/okl-0.24-raw.tmp"); return 61; } + const int b = static_cast(::syscall(SYS_openat, AT_FDCWD, "/etc/hostname", O_RDONLY)); + if (b >= 0) return 62; + return 60; +}}; + +child thp{"thp", [](std::vector) { + return static_cast(::prctl(PR_GET_THP_DISABLE, 0, 0, 0, 0)) == 1 ? 70 : 71; +}}; + +// Prove the existing file is readable before asking the kernel to truncate it. +// Missing files and unreachable grants are not proof. +int truncate_raw(const std::vector& args, bool by_open) { + if (args.size() != 1) return 67; + const char* path = args[0].c_str(); + const int readable = static_cast(::syscall(SYS_openat, AT_FDCWD, path, O_RDONLY)); + if (readable < 0) return 66; + ::close(readable); + errno = 0; + const long result = by_open ? ::syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_TRUNC) + : ::syscall(SYS_truncate, path, 0); + const int why = errno; + if (result >= 0) { + if (by_open) ::close(static_cast(result)); + return 64; } + return why == EACCES ? 63 : 65; +} +child truncate_by_name{"truncate-raw", [](std::vector args) { return truncate_raw(args, false); }}; +child truncate_by_open{"open-truncate-raw", [](std::vector args) { return truncate_raw(args, true); }}; +UTest openkal_0_23_test{"openkal_0_23", [] { // The pinned compiler's Linux headers can predate Landlock. This syscall // has number 444 on both architectures this implementation builds for. const long landlock = ::syscall(444, nullptr, 0, 1 /* _VERSION */); @@ -104,9 +116,9 @@ int main(int argc, char** argv) { "confinement is claimed only where Landlock can restrict file truncation"); const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); - if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } + if (n <= 0 || g_self[0] != '/') { fail("the program's own image is named"); return; } g_self[n] = 0; - if (!mkdtemp(g_dir)) { std::printf("FAIL: a directory for the test\n"); return 1; } + if (!mkdtemp(g_dir)) { fail("a directory for the test"); return; } char path[256]; std::snprintf(path, sizeof path, "%s/present.txt", g_dir); { const int fd = ::open(path, O_CREAT | O_WRONLY, 0600); (void)!::write(fd, "x", 1); ::close(fd); } @@ -124,7 +136,7 @@ int main(int argc, char** argv) { kal_dir rd{}; check(kal_fs_dir_attenuate(dir, &rights, &rd) == kal_ok, "the directory is attenuated, recorded"); const kal_preopen grant{ rd, "/granted", 8 }; - check(start("--read", &grant, 1, 0) == 0, "a program granted the recorded handle reads through it"); + check(start("read", &grant, 1, 0) == 0, "a program granted the recorded handle reads through it"); kal_fs_close_dir(rd); kal_fs_close_file(rec); char buf[4096] = {}; @@ -162,11 +174,11 @@ int main(int argc, char** argv) { grants[count] = kal_preopen{ a, names[count], std::strlen(names[count]) }; ++count; } - const int s = start("--write-raw", grants, count, kal::process::confine_flag.bits); - check(s == 60, "a confined program is refused by the kernel what its grants do not allow, without openkal"); - if (s != 60) std::printf(" the confined program reported %d\n", s); + const int s = start("write-raw", grants, count, kal::process::confine_flag.bits); + expect(s == 60) << "a confined program is refused by the kernel what its grants do not allow, without openkal" + << " (the confined program reported " << s << ")"; std::snprintf(path, sizeof path, "%s/present.txt", g_dir); - const char* truncations[] = { "--truncate-raw", "--open-truncate-raw" }; + const char* truncations[] = { "truncate-raw", "open-truncate-raw" }; for (const char* errand : truncations) { check(start(errand, grants, count, kal::process::confine_flag.bits, path) == 63, "a readable file in a read-only grant cannot be truncated without openkal"); @@ -179,14 +191,12 @@ int main(int argc, char** argv) { } // Economy. - check(start("--thp", nullptr, 0, kal::process::economy_flag.bits) == 70, + check(start("thp", nullptr, 0, kal::process::economy_flag.bits) == 70, "a program started for economy has no transparent huge pages"); - check(start("--thp", nullptr, 0, kal::process::performance_flag.bits) == 71, + check(start("thp", nullptr, 0, kal::process::performance_flag.bits) == 71, "and one started for performance has them as the system decides"); std::snprintf(path, sizeof path, "%s/present.txt", g_dir); ::unlink(path); std::snprintf(path, sizeof path, "%s/record", g_dir); ::unlink(path); ::rmdir(g_dir); - if (failures == 0) std::printf("openkal_0_23: ok\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/script_sees_its_name.cpp b/tests/script_sees_its_name.cpp index a1d8bc2..75140a0 100644 --- a/tests/script_sees_its_name.cpp +++ b/tests/script_sees_its_name.cpp @@ -10,12 +10,16 @@ #include #include #include +#include #include #include #include import openkal.types; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { kal_dir root() { @@ -27,9 +31,9 @@ kal_dir root() { } } -int main() { +UTest script_sees_its_name_test{"script_sees_its_name", [] { char dir[] = "/tmp/okl-script-XXXXXX"; - if (::mkdtemp(dir) == nullptr) { std::printf("FAIL: a scratch directory\n"); return 1; } + if (::mkdtemp(dir) == nullptr) { fail("a scratch directory"); return; } char script[256], beside[256]; std::snprintf(script, sizeof script, "%s/stub", dir); std::snprintf(beside, sizeof beside, "%s/beside", dir); @@ -38,7 +42,7 @@ int main() { const char text[] = "#!/bin/sh\nprintf '%s' \"$0\" > \"${0%/*}/beside\"\n"; const bool written = fd >= 0 && ::write(fd, text, sizeof text - 1) == static_cast(sizeof text - 1); if (fd >= 0) ::close(fd); - if (!written) { std::printf("FAIL: the script is written\n"); return 1; } + if (!written) { fail("the script is written"); return; } } const kal_dir r = root(); @@ -57,15 +61,6 @@ int main() { ::unlink(script); ::rmdir(dir); - int failures = 0; - if (e != kal_ok || terminated != 0 || status != 0) { - std::printf("FAIL: the script is started (rc %d, status %d)\n", e, status); - ++failures; - } - if (std::strcmp(seen, script) != 0) { - std::printf("FAIL: the script sees its own name as $0 (saw '%s')\n", seen); - ++failures; - } - if (failures == 0) std::printf("openkal-linux: a script is given its name\n"); - return failures == 0 ? 0 : 1; -} + expect(e == kal_ok && terminated == 0 && status == 0) << "the script is started"; + expect(eq(std::string(seen), std::string(script))) << "the script sees its own name as $0"; +}}; diff --git a/tests/search_only_base.cpp b/tests/search_only_base.cpp index 6063cdd..012f38d 100644 --- a/tests/search_only_base.cpp +++ b/tests/search_only_base.cpp @@ -18,12 +18,12 @@ import openkal.fs; import openkal.stream; import openkal.types; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -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 root() { for (kal_uintptr i = 0; i < kal::fs::preopen_count(); ++i) { @@ -35,7 +35,7 @@ kal_dir root() { } } -int main() { +UTest search_only_base_test{"search_only_base", [] { const auto work = kal::fs::working(); check(kal_fs_mkdir(work, "okl_search_only", 15) == kal_ok, "a directory is created"); kal_dir d{}; @@ -83,7 +83,4 @@ int main() { ::chmod("okl_search_only", 0755); ::unlink("okl_search_only/inside"); ::rmdir("okl_search_only"); - - if (failures == 0) std::printf("search_only_base: ok\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/sigpipe_not_passed_on.cpp b/tests/sigpipe_not_passed_on.cpp index f8bed46..621e042 100644 --- a/tests/sigpipe_not_passed_on.cpp +++ b/tests/sigpipe_not_passed_on.cpp @@ -12,22 +12,22 @@ #include #include #include +#include +#include #include #include import openkal.types; import openkal.env; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { struct sigaction g_started{}; [[gnu::constructor(100)]] void observe() { ::sigaction(SIGPIPE, nullptr, &g_started); } -int failures = 0; -void check(bool held, const char* what) { - if (!held) { std::printf("FAIL: %s\n", what); ++failures; } -} - kal_dir root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { kal_dir d{}; char n[8]; kal_uintptr l = 0; @@ -36,58 +36,74 @@ kal_dir root() { return kal_dir{}; } -char g_self[4096]; +// This program's own image, as the environment named it. +std::string self_image() { + char self[4096]; + const kal_intptr n = kal_env_image(self, sizeof self - 1); + if (n <= 0 || self[0] != '/') return {}; + return std::string(self, static_cast(n)); +} -// This program again, through openkal, asked how it was started: 7 ignored, -// 0 default. +// This program again, as the child `report', through openkal, asked how it was +// started: 7 ignored, 0 default, -1 not started. int started_child_reports() { + const std::string self = self_image(); + if (self.empty()) return -1; + const std::vector command = child_command("report"); + std::vector argv; + std::vector lens; + for (const auto& a : command) { argv.push_back(a.c_str()); lens.push_back(a.size()); } const kal_dir r = root(); - const char* argv[] = { g_self, "--report" }; - const kal_uintptr lens[] = { std::strlen(g_self), 8 }; const kal_spawn how{ r, r, nullptr, nullptr, 0, 0 }; kal_process p{}; - const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), argv, lens, 2, - nullptr, nullptr, 0, nullptr, &p); + const int e = kal_process_spawn(&how, self.c_str() + 1, self.size() - 1, argv.data(), lens.data(), + argv.size(), nullptr, nullptr, 0, nullptr, &p); int status = -1, terminated = 0; if (e == kal_ok) kal_process_wait(p, &status, &terminated); return e == kal_ok && terminated == 0 ? status : -1; } -} // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--report") == 0) - return g_started.sa_handler == SIG_IGN ? 7 : 0; - const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); - if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } - g_self[n] = 0; - if (argc > 1 && std::strcmp(argv[1], "--started-default") == 0) { - struct sigaction now{}; - ::sigaction(SIGPIPE, nullptr, &now); - if (now.sa_handler != SIG_IGN) return 2; // this program's own writes are not ended by it - return started_child_reports() == 0 ? 0 : 1; +// This program again, as the child `mode', started with SIGPIPE as +// `disposition' says: 0 when the program it then starts through openkal +// reported what was expected. +int started_with(void (*disposition)(int), const char* mode) { + const std::string self = self_image(); + const std::vector command = child_command(mode); + std::vector argv; + for (const auto& a : command) argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + const pid_t child = ::fork(); + if (child == 0) { + ::signal(SIGPIPE, disposition); + ::execv(self.c_str(), argv.data()); + ::_exit(93); } - if (argc > 1 && std::strcmp(argv[1], "--started-ignoring") == 0) - return started_child_reports() == 7 ? 0 : 1; + int st = 0; + ::waitpid(child, &st, 0); + return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st); +} +} // namespace - // This program again, started with SIGPIPE as `disposition' says: 0 when - // the program it then starts through openkal reported what was expected. - auto started_with = [](void (*disposition)(int), const char* mode) { - const pid_t child = ::fork(); - if (child == 0) { - ::signal(SIGPIPE, disposition); - ::execl(g_self, g_self, mode, nullptr); - ::_exit(93); - } - int st = 0; - ::waitpid(child, &st, 0); - return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st); - }; - const int d = started_with(SIG_DFL, "--started-default"); - check(d != 2, "this program's own writes are not ended by SIGPIPE"); - check(d == 0, "a program started with SIGPIPE at its default starts others with it at its default"); - check(started_with(SIG_IGN, "--started-ignoring") == 0, - "a program started with SIGPIPE ignored passes the ignore on, as it was given"); +child report{"report", [](std::vector) { + return g_started.sa_handler == SIG_IGN ? 7 : 0; +}}; - if (failures == 0) std::printf("openkal-linux: SIGPIPE ignored here, not passed on\n"); - return failures == 0 ? 0 : 1; -} +child started_default{"started-default", [](std::vector) { + struct sigaction now{}; + ::sigaction(SIGPIPE, nullptr, &now); + if (now.sa_handler != SIG_IGN) return 2; // this program's own writes are not ended by it + return started_child_reports() == 0 ? 0 : 1; +}}; + +child started_ignoring{"started-ignoring", [](std::vector) { + return started_child_reports() == 7 ? 0 : 1; +}}; + +UTest sigpipe_not_passed_on_test{"sigpipe_not_passed_on", [] { + if (self_image().empty()) { fail("the program's own image is named"); return; } + const int d = started_with(SIG_DFL, "started-default"); + expect(ne(d, 2)) << "this program's own writes are not ended by SIGPIPE"; + expect(eq(d, 0)) << "a program started with SIGPIPE at its default starts others with it at its default"; + expect(eq(started_with(SIG_IGN, "started-ignoring"), 0)) + << "a program started with SIGPIPE ignored passes the ignore on, as it was given"; +}}; diff --git a/tests/spawn_without_execveat.cpp b/tests/spawn_without_execveat.cpp index 4601ed7..2a31626 100644 --- a/tests/spawn_without_execveat.cpp +++ b/tests/spawn_without_execveat.cpp @@ -14,6 +14,9 @@ import openkal.types; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { kal_dir root() { @@ -37,10 +40,10 @@ bool refuse_execveat() { } } -int main() { +UTest spawn_without_execveat_test{"spawn_without_execveat", [] { if (!refuse_execveat()) { - std::printf("openkal-linux: seccomp is not available here; nothing to observe\n"); - return 0; + skip("seccomp is not available here; nothing to observe"); + return; } const kal_dir r = root(); const char* argv[] = { "sh", "-c", "exit 7" }; @@ -51,9 +54,10 @@ int main() { int status = -1, terminated = 0; if (e == kal_ok) kal_process_wait(p, &status, &terminated); if (e != kal_ok || terminated != 0 || status != 7) { - std::printf("FAIL: a program is started where execveat answers ENOSYS (rc %d, status %d)\n", e, status); - return 1; + expect(eq(e, static_cast(kal_ok))) << "a program is started where execveat answers ENOSYS"; + expect(eq(terminated, 0)) << "and it ends of itself"; + expect(eq(status, 7)) << "with the status it gave"; + return; } std::printf("openkal-linux: a program is started where execveat answers ENOSYS\n"); - return 0; -} +}}; diff --git a/tests/stop_word_take_over.cpp b/tests/stop_word_take_over.cpp index 6a4d05e..11aacd4 100644 --- a/tests/stop_word_take_over.cpp +++ b/tests/stop_word_take_over.cpp @@ -16,16 +16,34 @@ #include #include #include +#include +#include #include #include import openkal.types; import openkal.process; import openkal.task; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -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; } + +// Starts this program again as the child `name' and answers its wait status. +int run_copy(const char* name) { + const std::vector command = child_command(name); + std::vector argv; + for (const auto& a : command) argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + const pid_t copy = ::fork(); + if (copy == 0) { + ::execv("/proc/self/exe", argv.data()); + ::_exit(93); + } + int st = 0; + ::waitpid(copy, &st, 0); + return st; } } @@ -39,45 +57,47 @@ void wait_for_request(void* p) { w->woken = 1; } -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--waiter") == 0) { - const kal_u32* word = kal_process_stop_requested(); - if (word == nullptr) return 90; - waiter w{ word, 0 }; - kal_task t{}; - if (kal_task_start(wait_for_request, &w, &t) != kal_ok) return 94; - ::usleep(100000); // the context is waiting by now - ::kill(::getpid(), SIGTERM); - for (int i = 0; i < 2000 && !w.woken; ++i) ::usleep(1000); - if (!w.woken) return 95; - kal_task_join(t); - return 0; - } - if (argc > 1 && std::strcmp(argv[1], "--hang-up") == 0) { - // 0.18.1: the closing of the terminal is a request to end, taken over - // with the others, and the program ends as it would have. - const kal_u32* word = kal_process_stop_requested(); - if (word == nullptr) return 90; - ::kill(::getpid(), SIGHUP); - for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); - if (*word == 0) return 91; - kal_process_end_as_requested(); - return 92; - } - if (argc > 1 && std::strcmp(argv[1], "--end-as-requested") == 0) { - const kal_u32* word = kal_process_stop_requested(); - if (word == nullptr) return 90; - ::kill(::getpid(), SIGTERM); - for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); - if (*word == 0) return 91; - kal_process_end_as_requested(); - return 92; - } +child waiting{"waiter", [](std::vector) { + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) return 90; + waiter w{ word, 0 }; + kal_task t{}; + if (kal_task_start(wait_for_request, &w, &t) != kal_ok) return 94; + ::usleep(100000); // the context is waiting by now + ::kill(::getpid(), SIGTERM); + for (int i = 0; i < 2000 && !w.woken; ++i) ::usleep(1000); + if (!w.woken) return 95; + kal_task_join(t); + return 0; +}}; + +child hang_up{"hang-up", [](std::vector) { + // 0.18.1: the closing of the terminal is a request to end, taken over + // with the others, and the program ends as it would have. + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) return 90; + ::kill(::getpid(), SIGHUP); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + if (*word == 0) return 91; + kal_process_end_as_requested(); + return 92; +}}; +child end_as_requested{"end-as-requested", [](std::vector) { + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) return 90; + ::kill(::getpid(), SIGTERM); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + if (*word == 0) return 91; + kal_process_end_as_requested(); + return 92; +}}; + +UTest stop_word_take_over_test{"stop_word_take_over", [] { std::signal(SIGINT, SIG_IGN); const kal_u32* word = kal_process_stop_requested(); check(word != nullptr, "the stop word is armed"); - if (word == nullptr) return 1; + if (word == nullptr) return; ::kill(::getpid(), SIGINT); ::usleep(20000); @@ -87,36 +107,15 @@ int main(int argc, char** argv) { for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); check(*word != 0, "a request after arming sets the word, and the handler returns"); - const pid_t child = ::fork(); - if (child == 0) { - ::execl("/proc/self/exe", argv[0], "--end-as-requested", nullptr); - ::_exit(93); - } - int st = 0; - ::waitpid(child, &st, 0); + int st = run_copy("end-as-requested"); check(WIFSIGNALED(st) && WTERMSIG(st) == SIGTERM, "a program that ends as requested is reported to its starter as ended by the request"); - const pid_t hung = ::fork(); - if (hung == 0) { - ::execl("/proc/self/exe", argv[0], "--hang-up", nullptr); - ::_exit(93); - } - st = 0; - ::waitpid(hung, &st, 0); + st = run_copy("hang-up"); check(WIFSIGNALED(st) && WTERMSIG(st) == SIGHUP, "a hang-up is taken over as a request, and the program ends as it would have"); - const pid_t waiting = ::fork(); - if (waiting == 0) { - ::execl("/proc/self/exe", argv[0], "--waiter", nullptr); - ::_exit(93); - } - st = 0; - ::waitpid(waiting, &st, 0); + st = run_copy("waiter"); check(WIFEXITED(st) && WEXITSTATUS(st) == 0, "a context waiting upon the word with no bound is woken by the request"); - - if (failures == 0) std::printf("openkal-linux: the stop word is taken over and given back\n"); - return failures == 0 ? 0 : 1; -} +}}; From 3333b8f2b9e0a57f4f61fbd90a6527033f3692b1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 05:09:58 +0900 Subject: [PATCH 4/6] tests: a confined copy is granted /dev read-only The copy begins in mtest's entry, which takes a seed from std::random_device before the copy's errand runs; libc++ reads it from /dev/urandom, and under Landlock with no grant for /dev the copy ended before its errand (llvm job). The assertions are unchanged: /tmp stays read-only and /etc ungranted. --- tests/openkal_0_23.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp index 4edd00b..04cd5d2 100644 --- a/tests/openkal_0_23.cpp +++ b/tests/openkal_0_23.cpp @@ -161,8 +161,10 @@ UTest openkal_0_23_test{"openkal_0_23", [] { ro.flags = kal::fs::dir_read_only.bits; // What a program of this system reads to start --- its loader and // libraries, wherever the toolchain put them --- and /tmp, read-only; - // /etc is not granted. - static const char* const trees[] = { "tmp", "usr", "lib", "lib64", "home" }; + // /etc is not granted. /dev is granted read-only too: the test + // library's entry, which runs in the copy before its errand, takes a + // seed from std::random_device, and libc++ reads it from /dev/urandom. + static const char* const trees[] = { "tmp", "usr", "lib", "lib64", "home", "dev" }; kal_preopen grants[8]; static char names[8][16]; kal_uintptr count = 0; From 399268267239788915261a9a23df7f01d384c123 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 05:26:52 +0900 Subject: [PATCH 5/6] Two volumes are the kernel's refusal; each request counted in one exchange The README and the comment said two volumes were answered before the call; the kernel answers them with EXDEV, which is reported as kal_err_not_supported, and only a directory is refused before the call. The stop handler counts and skips zero in one compare-and-exchange. An add followed by a fix-up at the wrap could lose a count when a second handler ran between the two. --- README.md | 9 +++++---- src/fs.cpp | 10 +++++----- src/process.cpp | 15 ++++++++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 91edab4..dff3244 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,13 @@ 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` and two volumes with -`kal_err_not_supported` before the kernel is asked, because its own answers -(`EPERM`, `EXDEV`) name other conditions. +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 writes one where the count wraps to zero. +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 diff --git a/src/fs.cpp b/src/fs.cpp index f9cdc99..4b1fe2b 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -1080,11 +1080,11 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, } // 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. Two -// conditions are answered before the call, because the kernel's own answers -// name a different condition: a directory is refused with EPERM, which reads as -// a permission, and two volumes with EXDEV, which this interface states as an -// operation the pair cannot perform. +// 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); diff --git a/src/process.cpp b/src/process.cpp index 7aa3c04..0ad09b9 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1261,11 +1261,16 @@ void stop_handler(int signo) { __atomic_compare_exchange_n(&g_stop_signal, &none, signo, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED); // 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. The - // add is atomic, so handlers of two signals that interrupt one another each - // count; the fix-up after a wrap only ever writes one. - if (__atomic_add_fetch(&g_stop_word, 1u, __ATOMIC_RELEASE) == 0) - __atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE); + // 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 From 586799e720389b23afc7efa3d392b613b7d78540 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 05:31:12 +0900 Subject: [PATCH 6/6] openkal 0.25.0, as released --- mcpp.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcpp.toml b/mcpp.toml index f2ccf19..986987f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -67,7 +67,7 @@ provides-interfaces = [ ] [dependencies] -openkal = { git = "https://github.com/mcpplibs/openkal.git", branch = "feat/0.25.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