Commit the container the linux-x64 entries are honest in

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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-17 00:37:16 +02:00
commit 440709829d
4 changed files with 80 additions and 6 deletions

View file

@ -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.

View file

@ -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) +

View file

@ -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}`,

View file

@ -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