From 440709829deece8ba436b543ae274af4a950a26c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 17 Sep 2026 00:37:16 +0200 Subject: [PATCH] Commit the container the linux-x64 entries are honest in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The image the bridge is built and mutated in existed nowhere in the repository, and the recipe that stood in for it installed a C++ toolchain and no JavaScript runtime — so the container could not run the harness that checks the bridge it had just built. It is a file now, pinned to the same debian:13 digest as the release workflow, carrying ninja, Node, setarch and MsQuic's four runtime dependencies. The harness configures with `-G Ninja` on linux-x64, which is what scripts/build-quic-native.sh uses. CMake's Linux default is Makefiles, the image carries ninja rather than make, and the default configured nothing at all: the first run in the committed container died in cmake. It said so, because the control now prints what the child said, which is the whole point of that change. The seccomp requirement is stated where someone hitting it will read it — the bridge's README, beside the recipe — rather than being knowledge the runner assumes its caller already has. Co-Authored-By: Claude Opus 5 --- native/GameCult.Mesh.Quic.Native/README.md | 20 ++++++++++ .../tests/cultmesh_quic_native_tests.cpp | 5 ++- scripts/mutate-cultmesh.mjs | 23 +++++++++-- scripts/quic-native-linux-dev.Dockerfile | 38 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 scripts/quic-native-linux-dev.Dockerfile diff --git a/native/GameCult.Mesh.Quic.Native/README.md b/native/GameCult.Mesh.Quic.Native/README.md index 60531975..4653f86c 100644 --- a/native/GameCult.Mesh.Quic.Native/README.md +++ b/native/GameCult.Mesh.Quic.Native/README.md @@ -102,6 +102,26 @@ shipped beside it resolves without being installed system-wide. A Linux host still needs MsQuic's own runtime dependencies present: `libssl3t64`, `libnuma1`, `libxdp1`, `libnl-route-3-200`. +The development loop for it is one committed image, +[`scripts/quic-native-linux-dev.Dockerfile`](../../scripts/quic-native-linux-dev.Dockerfile), +pinned to the same `debian:13` digest the release workflow builds in. It carries +the toolchain, MsQuic's runtime dependencies, `setarch`, and Node, because the +mutation harness that drives the runtime-lifetime scenarios is a Node script and +a container that cannot run it cannot check the bridge it just built: + + docker build -t cultlib-quic-native-dev -f scripts/quic-native-linux-dev.Dockerfile scripts + docker run --rm --security-opt seccomp=unconfined -v F:\Projects\CultLib:/src -w /src \ + cultlib-quic-native-dev bash -lc "scripts/build-quic-native.sh && node scripts/mutate-cultmesh.mjs native" + +`--security-opt seccomp=unconfined` is load-bearing, not caution. The +ThreadSanitizer configuration needs the process's address space where it expects +it, so `scripts/mutate-cultmesh.mjs` re-executes those runs under `setarch -R`, +which asks the kernel for `personality(ADDR_NO_RANDOMIZE)`. Docker's default +seccomp profile denies that call. Without it ThreadSanitizer dies before `main` +with "unexpected memory mapping" — a configuration that never starts, which a +mutation harness would otherwise read as every mutant being killed. The harness +stops with that diagnosis rather than reporting kills it did not earn. + Where an artifact was built is part of what it is. The Linux binary is linked against Debian 13's glibc, so a build from anywhere else is a different artifact whatever the file name says. diff --git a/native/GameCult.Mesh.Quic.Native/tests/cultmesh_quic_native_tests.cpp b/native/GameCult.Mesh.Quic.Native/tests/cultmesh_quic_native_tests.cpp index 36794d22..2b2fa3a3 100644 --- a/native/GameCult.Mesh.Quic.Native/tests/cultmesh_quic_native_tests.cpp +++ b/native/GameCult.Mesh.Quic.Native/tests/cultmesh_quic_native_tests.cpp @@ -224,8 +224,9 @@ std::string HoldCloseOnce(int pollers) { for (auto& thread : threads) thread.join(); if (peak != pollers) - return "the library counted a peak of " + std::to_string(peak) + " host call(s) inside it, not " + - std::to_string(pollers) + "; the blocking crossing is not inside a call scope"; + return "the library's peak count of host calls inside it was " + std::to_string(peak) + ", not " + + std::to_string(pollers) + ": either the blocking crossing is not inside a call scope, or it " + + "did not block long enough for the pollers to be inside it together"; if (at_close != pollers) return "the close began its wait with " + std::to_string(at_close) + " host call(s) counted inside, not " + std::to_string(pollers) + diff --git a/scripts/mutate-cultmesh.mjs b/scripts/mutate-cultmesh.mjs index c8cced18..c75a9041 100644 --- a/scripts/mutate-cultmesh.mjs +++ b/scripts/mutate-cultmesh.mjs @@ -39,10 +39,21 @@ // reported killed from a win32-x64 release run where both in fact survive, and // die deterministically only under linux-x64 AddressSanitizer. // -// This script tests the platform it is running on. For the linux-x64 native -// entries, run it inside the Debian 13 container the bridge is built in, with -// cmake, ninja, a C++20 compiler and Node present; a Windows run reports those -// entries as skipped and says so. +// This script tests the platform it is running on, and a run that skipped +// entries exits 2 rather than 0: those rules were covered by nothing. For the +// linux-x64 native entries, run it inside the committed development image, +// `scripts/quic-native-linux-dev.Dockerfile`, which is the same debian:13 digest +// the bridge is built in and carries Node, ninja, setarch and MsQuic's runtime +// dependencies: +// +// docker build -t cultlib-quic-native-dev -f scripts/quic-native-linux-dev.Dockerfile scripts +// docker run --rm --security-opt seccomp=unconfined -v F:\Projects\CultLib:/src -w /src \ +// cultlib-quic-native-dev bash -lc "node scripts/mutate-cultmesh.mjs native" +// +// The seccomp flag is required, not cautious: the ThreadSanitizer configuration +// is re-executed under `setarch -R`, and Docker's default profile denies the +// personality call that needs. See `explainUnrunnable` below, and the bridge's +// README. // // node scripts/mutate-cultmesh.mjs [target...] @@ -625,6 +636,10 @@ function nativeScenariosPass() { try { execFileSync("cmake", [ "-S", source, "-B", build, + // The same generator scripts/build-quic-native.sh uses. CMake's Linux + // default is Makefiles, and the container the bridge is built in carries + // ninja rather than make, so the default configured nothing at all. + ...(platform === "linux-x64" ? ["-G", "Ninja"] : []), "-DCMAKE_BUILD_TYPE=RelWithDebInfo", `-DCMAKE_CXX_FLAGS=${flags}`, `-DCMAKE_EXE_LINKER_FLAGS=${flags}`, diff --git a/scripts/quic-native-linux-dev.Dockerfile b/scripts/quic-native-linux-dev.Dockerfile new file mode 100644 index 00000000..7c796749 --- /dev/null +++ b/scripts/quic-native-linux-dev.Dockerfile @@ -0,0 +1,38 @@ +# The linux-x64 development loop for the CultMesh native QUIC bridge: build it, +# run its scenarios, and run the mutation harness over it. Not a release +# builder — the shipped Linux artifact comes from the workflow (see +# docs/typescript-quic-realtime-cut.md). +# +# This image is what the harness's linux-x64 entries are honest about. Before it +# was committed, the recipe in the cut map installed a C++ toolchain and no +# JavaScript runtime, so the container the bridge was built in could not run the +# runner that mutates it at all. +# +# docker build -t cultlib-quic-native-dev -f scripts/quic-native-linux-dev.Dockerfile scripts +# docker run --rm --security-opt seccomp=unconfined -v F:\Projects\CultLib:/src -w /src \ +# cultlib-quic-native-dev bash -lc "scripts/build-quic-native.sh && node scripts/mutate-cultmesh.mjs native" +# +# `--security-opt seccomp=unconfined` is not optional and not caution. The +# ThreadSanitizer configuration needs the process's address space where it +# expects it, so the harness re-executes it under `setarch -R`, which asks the +# kernel for personality(ADDR_NO_RANDOMIZE). Docker's default seccomp profile +# denies that call, and without it ThreadSanitizer dies before main with +# "unexpected memory mapping" — a whole configuration failing to start, which a +# mutation harness reads as every mutant being killed. +# +# Pinned by digest, not by the moving tag: the artifact is the toolchain that +# made it. +FROM debian:13@sha256:f324c7ff54321e8d9c588493a20244965938ce0aa50bbd1022d38010e9ffc4b1 + +# cmake, ninja and g++ build the bridge and its scenario runner; curl, +# ca-certificates and dpkg fetch and unpack the pinned libmsquic deb; +# util-linux carries setarch; nodejs runs scripts/mutate-cultmesh.mjs. The four +# libraries are MsQuic's own runtime dependencies, which the bridge does not +# vendor. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash ca-certificates cmake curl dpkg g++ ninja-build nodejs util-linux \ + libssl3t64 libnuma1 libxdp1 libnl-route-3-200 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src