From 62feb2b3f207abbc583d1d1c3d5fff7dc7978ef3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 28 Aug 2026 11:57:42 +0800 Subject: [PATCH] Assert what this release changed, and say when the index pointer is behind The closure asks whether a program depending on every published package builds and runs. openkal-musl 0.6.0 answered three defects a consumer reported as openkal-linux#13, and none of them was among the things this program asked. Three observations added, each of which was red on the previous release: a redirection the caller performed reaches the started program --- it used not to, because openkal spells inheritance as a handle of zero and dup2 rebinds the C library's table rather than the running program's stream; an uncaught exception ends on SIGABRT --- it used to reach musl's a_crash(), which on x86_64 is hlt, and be reported as a segmentation fault. That is this ecosystem's own exit path: libc++abi's terminate handler ends in abort; asking after a started program without waiting for it returns. The program it starts is itself, with an argument, rather than /bin/sh. A criterion that shelled out would be asserting something about the sandbox and would report the absence of a shell as a defect of the release. AND A PREFLIGHT, BECAUSE THE FIRST TWO RUNS OF THIS FAILED FOR A REASON THAT HAD NOTHING TO DO WITH THE RELEASE. mcpp-index publishes a content-hash artifact and a rolling pointer. A client fetches the pointer from raw.githubusercontent.com, which serves a cached copy for some minutes after a push --- so a run started right after a merge resolves the PREVIOUS artifact and reports the package that was just added as not found, while claiming the index is current and was synced zero seconds ago. Both sentences are false and the report reads as a release that failed. Removing the local index directory does not touch it, and neither does removing mcpp's own registry; both were tried. What distinguishes "not pushed" from "cached" is a route that does not go through the same cache: the pointer repository's own commit already named the new artifact while the raw URL still served the old one. That comparison is now made before an hour of building is spent on an answer about the wrong index. The mirror is an argument as well, defaulting to what a runner outside China wants; a machine inside it passes mirror=CN. --- tools/sandbox-closure.sh | 123 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 4 deletions(-) diff --git a/tools/sandbox-closure.sh b/tools/sandbox-closure.sh index 6d3304e..876ccd8 100644 --- a/tools/sandbox-closure.sh +++ b/tools/sandbox-closure.sh @@ -52,9 +52,15 @@ shift # script. MCPP=2026.8.27.1 KIT=0.1.1 -MUSL=0.5.0 +MUSL=0.6.0 LINUX=0.6.0 -RUNTIME=0.3.0 +RUNTIME=0.3.1 +MIRROR=GLOBAL +# ⚠️ WHICH MIRROR THE SANDBOX DOWNLOADS FROM, AND IT IS NOT THE SAME ANSWER +# EVERYWHERE. A runner outside China reaches the global one fastest and a +# machine inside it does not; the two are a property of where this is run rather +# than of what is being checked, so it is an argument with the runner's answer as +# the default. for pair in "$@"; do case "$pair" in @@ -63,6 +69,7 @@ for pair in "$@"; do musl=*) MUSL="${pair#*=}" ;; linux=*) LINUX="${pair#*=}" ;; runtime=*) RUNTIME="${pair#*=}" ;; + mirror=*) MIRROR="${pair#*=}" ;; *) echo "unknown pair: $pair" >&2; exit 2 ;; esac done @@ -70,6 +77,40 @@ done xlings subos list 2>/dev/null | grep -q " $subos " \ || { echo "::error::the environment $subos does not exist"; exit 1; } +# ⚠️⚠️ THE INDEX IS REACHED THROUGH A POINTER THAT IS CACHED UPSTREAM, AND +# REMOVING THE LOCAL COPY DOES NOT TOUCH IT. +# +# mcpp-index publishes a content-hash artifact and a rolling pointer file. A +# client fetches the pointer from `raw.githubusercontent.com`, which serves a +# cached copy for some minutes after a push. So a run started right after a merge +# resolves the PREVIOUS artifact --- and reports the package that was just added +# as +# +# E_NOT_FOUND: package 'x@v' not found in the synced index +# (… mcpplibs@artifact: …), synced 0 seconds ago +# +# which reads as a release that failed and is nothing of the kind. Measured +# 2026-08-28, twice: the pointer repository's own commit already named the new +# artifact while the raw URL still served the old one. +# +# ⇒ The two are compared here, before an hour of building is spent on an answer +# about the wrong index. The API is not cached the way the raw host is, which is +# what makes the comparison possible at all. +head="$(curl -fsSL --max-time 60 https://api.github.com/repos/mcpplibs/mcpp-index/commits/main \ + | sed -n 's/.*"sha": "\([0-9a-f]\{7\}\).*/\1/p' | head -1)" +seen="$(curl -fsSL --max-time 60 -H 'Cache-Control: no-cache' \ + "https://raw.githubusercontent.com/xlings-res/mcpp-index/main/mcpp-index-pointers.json" \ + | sed -n 's/.*"index_version": "\([^"]*\)".*/\1/p' | head -1)" +if [ -n "$head" ] && [ -n "$seen" ] && [ "$head" != "$seen" ]; then + echo "::error::the index pointer is behind: it names $seen and mcpp-index main is $head" + echo " the artifact for $head may already exist; what is stale is the cached" + echo " pointer a client reads. Wait a few minutes and run this again ---" + echo " building against $seen would answer for the previous index." + exit 1 +fi +[ -n "$seen" ] && echo " the index pointer names $seen, and mcpp-index main is ${head:-unknown}" + + cat < /dev/null 2>&1 || true # index repository carries the name --- measured: local, scode and xim all # answer, and xlings refuses rather than choosing. The engine is published in # xim. +xlings config --mirror __MIRROR__ > /dev/null 2>&1 || true xlings install "xim:mcpp@__MCPP__" -y -g # ⚠️⚠️ INSTALLING IS NOT BECOMING WHAT RUNS. In an environment that already has @@ -117,7 +160,7 @@ xlings install "xim:mcpp@__MCPP__" -y -g # this check would build the whole ecosystem with the OLD engine and report the # release as verified. xlings use mcpp __MCPP__ || true -mcpp self config --mirror GLOBAL +mcpp self config --mirror __MIRROR__ got="$(mcpp --version | awk '{print $2}')" [ "$got" = "__MCPP__" ] || { echo "::error::mcpp is $got, not __MCPP__"; exit 1; } echo " mcpp is $got" @@ -152,6 +195,10 @@ cat > src/main.cpp <<'CPP' // in, which is what makes the include path under examination the one used. #include #include +#include +#include +#include +#include #include #include #include @@ -159,6 +206,8 @@ cat > src/main.cpp <<'CPP' #include #include +extern char** environ; + #include #include import std; @@ -171,7 +220,13 @@ static int hidden = 7; static int weak = 11; struct weak_alias { int value; }; -int main() { +int main(int argc, char** argv) { + // ⭐ THE PROGRAM THIS ONE STARTS IS ITSELF. A criterion that shelled out to + // `/bin/sh` would be asserting something about the sandbox rather than about + // the C library, and would report the absence of a shell as a defect of the + // release. + if (argc > 1) { (void)!::write(1, argv[1], ::strlen(argv[1])); return 0; } + std::vector v{4, 2, 7}; std::ranges::sort(v); std::print("sorted:"); @@ -224,6 +279,58 @@ int main() { const bool reaped = kid > 0 && ::waitpid(kid, &status, 0) == kid && WIFEXITED(status) && WEXITSTATUS(status) == 23; std::println("the calling image is duplicated: {}", reaped); + + // ⭐⭐ AND WHAT openkal-musl 0.6.0 ANSWERS, WHICH A CONSUMER REPORTED AND + // NOT THIS ECOSYSTEM (openkal-linux#13). Each of the three was measured + // from outside, on packages that had been published and were green here. + + // ① A redirection the caller performed reaches the started program. It used + // not to: `dup2` rebinds the C library's own table and cannot rebind the + // running program's stream, so the started program wrote to the stream + // THIS program had been started with and the file stayed empty. + std::fflush(stdout); + const int saved = ::dup(1); + const int sink = ::open("/tmp/sink.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); + bool redirected = false; + if (saved >= 0 && sink >= 0) { + ::dup2(sink, 1); + ::close(sink); + pid_t echoer = -1; + char* av[] = { argv[0], const_cast("carried-into-the-file"), nullptr }; + const int se = ::posix_spawn(&echoer, argv[0], nullptr, nullptr, av, ::environ); + if (se == 0) { int est = 0; ::waitpid(echoer, &est, 0); } + std::fflush(stdout); + ::dup2(saved, 1); + ::close(saved); + char buf[64] = {}; + const int rd = ::open("/tmp/sink.txt", O_RDONLY); + const auto n = rd >= 0 ? ::read(rd, buf, sizeof buf - 1) : -1; + if (rd >= 0) ::close(rd); + redirected = se == 0 && n > 0 && memcmp(buf, "carried-into-the-file", 21) == 0; + } + std::println("a redirection reaches the started program: {}", redirected); + + // ② An uncaught exception ends the program the way a C library says it + // does. It used to reach musl's `a_crash()` --- `hlt` --- and be reported + // as a segmentation fault, which is this runtime's own exit path: the + // terminate handler in libc++abi ends in `abort`. + const pid_t thrower = ::fork(); + if (thrower == 0) throw std::runtime_error("deliberate, and uncaught"); + int tst = 0; + ::waitpid(thrower, &tst, 0); + std::println("an uncaught exception ends on SIGABRT: {}", + WIFSIGNALED(tst) && WTERMSIG(tst) == SIGABRT); + + // ③ Asking after a started program without waiting for it returns. It used + // to block, which is the one thing that flag exists to prevent. + const pid_t slow = ::fork(); + if (slow == 0) { ::usleep(300000); ::_exit(9); } + int sst = 0; + long spins = 0; + pid_t got = 0; + while ((got = ::waitpid(slow, &sst, WNOHANG)) == 0 && spins < 100000) spins++; + std::println("asking without waiting returned {} times, then named it: {}", + spins > 0, got == slow && WIFEXITED(sst) && WEXITSTATUS(sst) == 9); return 0; } CPP @@ -240,6 +347,13 @@ grep -q 'kit parses true and refuses true: 255 8080' /tmp/out.log # reported nothing. grep -qE 'sockets: port [1-9][0-9]* connected true readable true carried true' /tmp/out.log grep -q 'the calling image is duplicated: true' /tmp/out.log +# ⭐ THE THREE openkal-musl 0.6.0 ANSWERS. Each was red on 0.5.0. +grep -q 'a redirection reaches the started program: true' /tmp/out.log \ + || { echo "::error::a started program did not receive the caller's redirection"; exit 1; } +grep -q 'an uncaught exception ends on SIGABRT: true' /tmp/out.log \ + || { echo "::error::an uncaught exception did not end the way a C library says"; exit 1; } +grep -q 'asking without waiting returned true times, then named it: true' /tmp/out.log \ + || { echo "::error::waitpid(WNOHANG) blocked, or did not name the program"; exit 1; } say "what the engine believes each layer is" # ⭐ THE VERSION IS THE FIELD THAT DISCRIMINATES. That the engine knows a layer @@ -297,5 +411,6 @@ SCRIPT="${SCRIPT//__KIT__/$KIT}" SCRIPT="${SCRIPT//__MUSL__/$MUSL}" SCRIPT="${SCRIPT//__RUNTIME__/$RUNTIME}" SCRIPT="${SCRIPT//__LINUX__/$LINUX}" +SCRIPT="${SCRIPT//__MIRROR__/$MIRROR}" xlings subos use "$subos" --sandbox --cmd "$SCRIPT"