Land the QUIC Cut 3 eighth fix batch: release config, whole-duration seam, pollhammer live objects, zero-timeout bound, closerace starvation fix

Self's ruling of 2026-09-22 named six findings against Soul's eighth pass.
This lands the code side of all six:

- The wait-record seam (CULTMESH_QUIC_DEBUG_RECORD_WAIT) now wraps the whole
  duration handed to `wait_for`, not the raw `timeout_ms` before it is
  converted to milliseconds, so arithmetic applied after that conversion can
  no longer live outside what the seam records. Its "unset" sentinel moves to
  a separate `debug_wait_recorded` flag so a recorded -1 could never be
  confused with nothing recorded.
- `pollhammer` also runs against a runtime holding a live connection to an
  unroutable address (RFC 5737 TEST-NET-1), a best-effort stream on it, and a
  real (never-connected) listener, closing the gap where the wait's predicate
  was only ever exercised on an empty runtime.
- A new `zerotimeout` scenario pins that `timeout_ms <= 0` returns within
  50 ms, probing 0, -1 and INT32_MIN.
- `closerace`'s settle no longer sleeps a guessed duration: where the
  development seam exists it waits for `debug_peak_calls` to reach the poller
  count (reset before any poller thread exists, so nothing can zero a count a
  poller already raised) before a short fixed margin closes the last gap
  between being counted and being inside the condition wait.
- `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms so the wall-clock
  scenarios can catch arithmetic applied outside the seam's own argument,
  which `waitseam`'s equality check structurally cannot see.

scripts/mutate-cultmesh.mjs gains a plain `release` configuration on both
platforms (CULTMESH_QUIC_DEBUG_ASSERTS=OFF, CMAKE_BUILD_TYPE=Release) running
`polltimeout`, `pollbusy`, `pollhammer` and `zerotimeout`, and seven new
mutation entries: R1 (the release macro divides the duration by 10), W1-W3
(600 ms added, a 500 ms floor, and a 1000/909 scale, each applied outside the
seam's own argument), P1 (the idle predicate also ends on a live connection),
and Z1-Z2 (a 500 ms sleep and a 300 ms spin on a non-positive timeout). Two
stale comments are corrected: the scenario list above `assertScenarios` now
names `pollhammer` among the clock-sensitive entries, and a historical "1300"
probe value that no longer matches the test file is removed. The header's
section 7 intro now says the seam serves both the quiesce and the wait
recording, not quiesce alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 12:27:56 +02:00
commit 5646ff7351
4 changed files with 463 additions and 98 deletions

View file

@ -33,6 +33,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
@ -43,6 +44,7 @@
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
@ -88,24 +90,27 @@
// decrements the count and notifies. That is the ordinary exit every export
// makes, and it is the bridge's wait — not the hold — that keeps the runtime
// alive underneath it.
// - RECORD_WAIT wraps the exact `timeout_ms` handed to the condition wait in
// `cultmesh_quic_next_event`, in place, as a pass-through: it stores the
// value and returns it unchanged, so a mutation to what is actually waited
// on changes what this reports rather than the two ever being able to
// diverge. A scenario reads it and asserts equality against the timeout it
// asked for, deterministically and with no timing tolerance — the method
// Self's ruling of 2026-09-22 asks for in place of inferring the same fact
// from how long a call took to return.
// - RECORD_WAIT wraps the whole duration handed to the condition wait in
// `cultmesh_quic_next_event`, in place, as a pass-through: it stores that
// duration's count and returns the duration unchanged, so a mutation to
// what is actually waited on changes what this reports rather than the two
// ever being able to diverge. It wraps `timeout_ms` after conversion to
// `std::chrono::milliseconds`, not before — Self's ruling of 2026-09-22,
// after arithmetic applied to the converted duration survived a seam that
// only recorded the raw integer underneath it. A scenario reads it and
// asserts equality against the timeout it asked for, deterministically and
// with no timing tolerance, in place of inferring the same fact from how
// long a call took to return.
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
#define CULTMESH_QUIC_DEBUG_PEAK(count) DebugRaisePeak(count)
#define CULTMESH_QUIC_DEBUG_AT_CLOSE(count) DebugRecordCallsAtClose(count)
#define CULTMESH_QUIC_DEBUG_HELD(lock) DebugHoldCall(lock)
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms) DebugRecordWait(timeout_ms)
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) DebugRecordWait(duration)
#else
#define CULTMESH_QUIC_DEBUG_PEAK(count) ((void)0)
#define CULTMESH_QUIC_DEBUG_AT_CLOSE(count) ((void)0)
#define CULTMESH_QUIC_DEBUG_HELD(lock) (false)
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms) (timeout_ms)
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) (duration)
#endif
namespace {
@ -249,6 +254,13 @@ std::mutex debug_hold_gate;
std::condition_variable debug_hold_signal;
bool debug_hold_armed = false;
std::atomic<int32_t> debug_last_wait_ms{-1};
// Separate from the value above on purpose: once the seam records the whole
// duration (not just a millisecond count already known to be positive), a
// mutation could in principle make a wait get recorded at exactly -1, and an
// int32_t alone cannot tell that apart from "nothing recorded yet". This flag
// is the "recorded" bit; the getter below is the only thing that still hands
// out -1 for the unset case.
std::atomic<bool> debug_wait_recorded{false};
void DebugRaisePeak(int count) {
int peak = debug_peak_calls.load(std::memory_order_relaxed);
@ -270,14 +282,18 @@ bool DebugHoldCall(std::unique_lock<std::mutex>& lock) {
return true;
}
// A pass-through: it stores `timeout_ms` and returns it unmodified, so wrapping
// it around the exact expression handed to the condition wait makes the two
// impossible to desync by construction. Whatever that expression evaluates to
// — the plain argument today, or a derivation a future change or a mutation
// introduces — is what gets recorded, because it is the same evaluation.
int32_t DebugRecordWait(int32_t timeout_ms) {
debug_last_wait_ms.store(timeout_ms);
return timeout_ms;
// A pass-through: it stores the duration's count and returns the duration
// unmodified, so wrapping it around the whole expression handed to the
// condition wait makes the two impossible to desync by construction. Whatever
// that expression evaluates to — the plain converted argument today, or a
// derivation a future change or a mutation introduces inside this call — is
// what gets recorded, because it is the same evaluation. Anything applied
// outside this call, after it returns, is what the wall-clock scenarios exist
// to catch instead; see their generous-tolerance comment.
std::chrono::milliseconds DebugRecordWait(std::chrono::milliseconds duration) {
debug_last_wait_ms.store(static_cast<int32_t>(duration.count()));
debug_wait_recorded.store(true);
return duration;
}
#endif
@ -1199,7 +1215,7 @@ CULTMESH_API int32_t cultmesh_quic_next_event(
// sooner, and `woken` is which of the two happened. RECORD_WAIT wraps
// this exact argument; see the development seam note near its macros.
const bool woken = runtime->signal.wait_for(
lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),
lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),
[runtime] { return !runtime->events.empty() || runtime->closing; });
// Development builds only; a no-op unless a scenario has armed the hold.
// A held call returns the same 0 an empty queue would have, and makes no
@ -1487,10 +1503,11 @@ CULTMESH_API int32_t cultmesh_quic_debug_calls_at_close(void) {
}
CULTMESH_API int32_t cultmesh_quic_debug_last_wait_ms(void) {
return debug_last_wait_ms.load();
return debug_wait_recorded.load() ? debug_last_wait_ms.load() : -1;
}
CULTMESH_API void cultmesh_quic_debug_reset_last_wait_ms(void) {
debug_wait_recorded.store(false);
debug_last_wait_ms.store(-1);
}
#endif

View file

@ -316,9 +316,10 @@ CULTMESH_QUIC_API int32_t cultmesh_quic_last_status(void* runtime);
*
* Present only in a library configured with `CULTMESH_QUIC_DEBUG_ASSERTS`, which
* a shipped build never is. It exists for the runtime-lifetime scenarios: the
* quiesce is a promise about host calls that are inside the library, and a
* scenario that can only sleep and hope cannot tell a bridge that counts them
* from one that does not.
* quiesce is a promise about host calls that are inside the library, and the
* wait scenarios need the same kind of promise about the exact duration the
* bridge hands its own condition wait. A scenario that can only sleep and hope
* cannot tell a bridge that counts and waits correctly from one that does not.
*
* It is process-wide, not per-runtime, because the numbers outlive the runtime
* they describe: `cultmesh_quic_runtime_close` frees the runtime, and the count
@ -341,12 +342,14 @@ CULTMESH_QUIC_API int32_t cultmesh_quic_debug_peak_calls(void);
* since the hold was armed. This is the number the wait exists for. */
CULTMESH_QUIC_API int32_t cultmesh_quic_debug_calls_at_close(void);
/* The `timeout_ms` most recently handed to the condition wait inside
* `cultmesh_quic_next_event`, recorded at the point the wait is entered rather
* than observed from outside. A poll whose queue is not empty, or whose
* `timeout_ms` is not greater than zero, never enters that wait and leaves this
* unchanged. -1 until the first such wait, or after
* `cultmesh_quic_debug_reset_last_wait_ms`.
/* The whole duration, in milliseconds, most recently handed to the condition
* wait inside `cultmesh_quic_next_event`, recorded at the point the wait is
* entered rather than observed from outside. A poll whose queue is not empty,
* or whose `timeout_ms` is not greater than zero, never enters that wait and
* leaves this unchanged. -1 until the first such wait, or after
* `cultmesh_quic_debug_reset_last_wait_ms` — tracked by a separate internal
* flag, not by -1 being stored here, so a wait that ever recorded exactly -1
* itself could not be confused with nothing having been recorded.
*
* This exists because a wall-clock probe cannot tell the host's timeout from
* any function of it that is the identity at the probe's own value — a floor,

View file

@ -25,6 +25,8 @@
// cultmesh_quic_native_tests holdtimeout [iterations]
// cultmesh_quic_native_tests waitseam [iterations]
// cultmesh_quic_native_tests pollhammer [iterations]
// cultmesh_quic_native_tests zerotimeout [iterations]
// cultmesh_quic_native_tests payloadfit [iterations]
//
// `closerace` hunts the race: many pollers, many iterations, and whatever the
// schedule gives. `holdclose` takes the guessing out of it — the bridge's
@ -124,6 +126,17 @@ std::string CloseRaceOnce(int pollers) {
std::atomic<bool> closing_started{false};
std::atomic<int> left_before_the_close{0};
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
// Reset before any poller thread exists, so nothing here can race a
// `CallScope` that has already been counted: `cultmesh_quic_next_event`
// raises `debug_peak_calls` the moment its `CallScope` is constructed, at
// the very top of the call, and a reset performed even microseconds after
// that construction would zero a count no further call is coming to raise
// again, stranding the settle loop below at "not yet" until its deadline.
cultmesh_quic_debug_hold_calls(1);
cultmesh_quic_debug_hold_calls(0);
#endif
std::vector<std::thread> threads;
threads.reserve(static_cast<size_t>(pollers));
for (int index = 0; index < pollers; ++index) {
@ -153,10 +166,36 @@ std::string CloseRaceOnce(int pollers) {
}
while (entering.load() < pollers) std::this_thread::yield();
// Settle so the pollers are inside the blocking wait rather than on their way
// to it. A poller that has not entered yet is a call beginning during the
// close, which is the host's to avoid, not the bridge's to survive.
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
// A fixed settle races CPU starvation: under enough contending threads a
// poller that has only incremented `entering` can sit preempted for far
// longer than any sleep this scenario could afford, so the close would
// begin before it ever reached the blocking wait — not the race this
// scenario exists to run, just a fixture that gave up too soon. Where the
// development seam is compiled in, this reads the bridge's own count
// instead of guessing at a duration: the settle ends only once every
// poller is actually counted inside, however long the scheduler took.
const auto counted_deadline =
std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (cultmesh_quic_debug_peak_calls() < pollers &&
std::chrono::steady_clock::now() < counted_deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
// `active_calls` — what `peak_calls` reflects — is raised the moment a
// poller's `CallScope` is constructed, at the top of
// `cultmesh_quic_next_event`, not when it reaches the condition wait a few
// statements later. Counted-inside is the expensive part to wait for under
// starvation (getting the scheduler to run a thread at all); a short fixed
// settle covers the cheap remainder (a lock acquisition and an empty-queue
// check) once every poller is already scheduled and running.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
#else
// No seam in this configuration (the sanitizer build deliberately ships
// without one), so the settle falls back to a longer fixed window rather
// than the 50 ms that starved under 16 contending burners. It is still a
// window, not a guarantee — the honest limit of a config with no way to
// ask the bridge what it has actually counted.
std::this_thread::sleep_for(std::chrono::milliseconds(500));
#endif
closing_started.store(true);
cultmesh_quic_runtime_close(runtime);
@ -194,9 +233,15 @@ constexpr int kEarlyToleranceMs = 20;
// — `waitseam` catches those by equality, with no clock involved — so this only
// has to catch a wait that is wrong by a lot: a constant of the bridge's own, a
// wait that never happens, a hold that parks a call its own timeout already
// ended. Each of those misses by seconds, not milliseconds, so one flat, wide
// margin serves every probe this file times, on a machine under real load.
constexpr int kGenerousLateToleranceMs = 800;
// ended, or the W1-W3 family Self's ruling of 2026-09-22 added, which apply
// their arithmetic to the duration `wait_for` actually receives *after* the
// seam has already recorded it and so cannot be caught by `waitseam`'s
// equality check at all. 400 ms sits above the worst overshoot a 16-burner
// stress pass measured on a loaded win32 host (180 ms; 27 ms on linux) and
// below every one of W1-W3's smallest offsets (485 ms), so one flat margin
// still serves every probe this file times without going blind to the family
// it was widened for.
constexpr int kGenerousLateToleranceMs = 400;
struct TimedPoll {
int32_t result;
@ -917,7 +962,109 @@ int WaitSeam(int iterations) {
// loop, and it leaves no listener behind.
constexpr int32_t kHammerPollMs = 1000;
std::string PollHammerOnce(bool with_recorded_error) {
// A self-signed ECDSA P-256 PKCS12, generated once with OpenSSL and embedded so
// this scenario needs no crypto library of its own and no network fetch:
// openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
// -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=cultmesh-quic-native-tests"
// openssl pkcs12 -export -out test.p12 -inkey key.pem -in cert.pem -passout pass:
// Used only to give pollhammer's live-objects run a listener that can actually
// load a credential; nothing ever connects to it, so it never accepts and never
// queues an event this scenario would have to tell apart from a bug.
constexpr std::array<uint8_t, 1088> kSelfSignedPkcs12{{
0x30, 0x82, 0x04, 0x3c, 0x02, 0x01, 0x03, 0x30, 0x82, 0x03, 0xf2, 0x06,
0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x82,
0x03, 0xe3, 0x04, 0x82, 0x03, 0xdf, 0x30, 0x82, 0x03, 0xdb, 0x30, 0x82,
0x02, 0x8a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07,
0x06, 0xa0, 0x82, 0x02, 0x7b, 0x30, 0x82, 0x02, 0x77, 0x02, 0x01, 0x00,
0x30, 0x82, 0x02, 0x70, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
0x01, 0x07, 0x01, 0x30, 0x5f, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
0x0d, 0x01, 0x05, 0x0d, 0x30, 0x52, 0x30, 0x31, 0x06, 0x09, 0x2a, 0x86,
0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0c, 0x30, 0x24, 0x04, 0x10, 0x48,
0xdf, 0x04, 0x5a, 0x31, 0x34, 0x6d, 0xe0, 0x7c, 0xde, 0x3c, 0xa2, 0xde,
0xf2, 0x8d, 0x20, 0x02, 0x02, 0x08, 0x00, 0x30, 0x0c, 0x06, 0x08, 0x2a,
0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x09, 0x05, 0x00, 0x30, 0x1d, 0x06,
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a, 0x04, 0x10,
0x44, 0x04, 0xd9, 0x9b, 0x99, 0x3a, 0xcb, 0x89, 0xc0, 0xcb, 0x53, 0xe3,
0xe3, 0x5b, 0xb4, 0x48, 0x80, 0x82, 0x02, 0x00, 0x92, 0xcc, 0x04, 0xcf,
0x7d, 0x58, 0xec, 0x68, 0x1a, 0x1b, 0xd5, 0x3b, 0x17, 0x57, 0x22, 0xd5,
0x45, 0x00, 0xff, 0x05, 0x0f, 0x0d, 0x18, 0x8b, 0x72, 0x5b, 0xcc, 0xd9,
0x87, 0xf4, 0x99, 0x44, 0x7c, 0xd6, 0xc3, 0x02, 0x2a, 0xaf, 0xd9, 0x10,
0x14, 0xd3, 0x9f, 0x1a, 0x2c, 0x7b, 0x4f, 0xe6, 0x0b, 0x1d, 0x7f, 0xb9,
0xf4, 0x01, 0x5c, 0x4b, 0xd5, 0xa1, 0x57, 0xb0, 0x3d, 0xe0, 0xbc, 0x70,
0x72, 0xe0, 0xe9, 0xb8, 0x9a, 0xde, 0x68, 0xfe, 0x7c, 0xc7, 0xca, 0xcc,
0x61, 0x32, 0x88, 0x72, 0xdd, 0x97, 0x28, 0x7b, 0x5b, 0x0d, 0x30, 0xf5,
0xa2, 0xd1, 0xe8, 0x89, 0xd4, 0x1d, 0x88, 0x28, 0xd2, 0xc3, 0x02, 0x93,
0xa1, 0x27, 0xfb, 0x4a, 0xe7, 0xdc, 0xac, 0xf3, 0x0d, 0xa9, 0xd3, 0x14,
0x37, 0x71, 0xb7, 0xe5, 0xaf, 0xdb, 0xb6, 0xda, 0x42, 0xb7, 0x24, 0x7a,
0x94, 0xad, 0x73, 0x79, 0xde, 0x21, 0xb9, 0x13, 0x63, 0x98, 0x33, 0x62,
0x4a, 0x36, 0xb8, 0x6d, 0x14, 0x8b, 0x84, 0xf0, 0x8e, 0x55, 0xcc, 0x95,
0x14, 0x54, 0xcc, 0x68, 0x4b, 0x09, 0x24, 0xb4, 0x79, 0x49, 0x05, 0xc7,
0x0e, 0xfe, 0xed, 0x23, 0x8c, 0x87, 0xc6, 0xc3, 0x76, 0x25, 0xd9, 0xd6,
0xae, 0x5a, 0xf4, 0x86, 0x74, 0xb8, 0x44, 0x22, 0xe8, 0xfc, 0xa5, 0xc2,
0x25, 0xbd, 0x84, 0xfd, 0x92, 0x96, 0xe2, 0x66, 0xa9, 0xb0, 0x1f, 0xd3,
0x8d, 0xf7, 0x1c, 0xce, 0xed, 0x09, 0x11, 0x61, 0xec, 0x2d, 0xa4, 0x9d,
0xb9, 0xdb, 0xd4, 0x2e, 0xa4, 0xff, 0xe6, 0x09, 0x8e, 0x9a, 0x0b, 0xdf,
0x30, 0xf4, 0xea, 0xe6, 0x09, 0x2d, 0x16, 0xec, 0x5d, 0xb6, 0x39, 0xda,
0xd4, 0x43, 0x41, 0x95, 0xc8, 0x32, 0xd2, 0xac, 0x17, 0x58, 0x05, 0x7e,
0x10, 0x24, 0xae, 0xd6, 0x58, 0x80, 0x0a, 0x29, 0xcb, 0x4b, 0x78, 0x62,
0xbe, 0xf6, 0x6c, 0x7d, 0x44, 0x07, 0x7b, 0xfb, 0xd9, 0xb6, 0x74, 0x75,
0x9f, 0xe6, 0xb4, 0xc3, 0x54, 0x53, 0x46, 0xdb, 0xf2, 0xa1, 0x47, 0xb8,
0xf1, 0xff, 0x01, 0xba, 0xde, 0xf9, 0xca, 0xd6, 0xc0, 0x38, 0x40, 0xd0,
0x86, 0x72, 0xc8, 0xcf, 0xdd, 0xd6, 0xf6, 0x36, 0xe0, 0xf3, 0x2e, 0x95,
0x23, 0xdb, 0x99, 0x72, 0x1c, 0xa6, 0xa0, 0xf7, 0x17, 0x93, 0x1e, 0x20,
0x6c, 0x75, 0x25, 0x0e, 0x1c, 0xab, 0xa4, 0xce, 0x35, 0xd4, 0xf4, 0xc6,
0x63, 0x89, 0x0d, 0xa9, 0x91, 0x0b, 0x7e, 0xce, 0x7d, 0x4d, 0x5e, 0xbf,
0xfd, 0xa9, 0xbd, 0xb6, 0x1e, 0x2b, 0x08, 0x5d, 0x7a, 0xdd, 0x61, 0xdf,
0x24, 0xe0, 0xff, 0x4b, 0xeb, 0xa2, 0xeb, 0xb6, 0x2f, 0x2c, 0xbf, 0x9b,
0x5f, 0x6a, 0x1a, 0x33, 0xae, 0x02, 0x6d, 0xbf, 0x8e, 0x6f, 0xeb, 0xda,
0x17, 0x88, 0x79, 0xf8, 0xe0, 0x1c, 0x1d, 0x64, 0x7c, 0xc3, 0xc2, 0xc1,
0xce, 0x12, 0x67, 0x74, 0xbc, 0xef, 0x9f, 0x2f, 0x35, 0xef, 0x40, 0x24,
0xc1, 0x5f, 0x82, 0xc9, 0x00, 0x2c, 0xbf, 0x2b, 0xbf, 0x64, 0x39, 0x63,
0x0c, 0xcd, 0x6f, 0xe5, 0x18, 0xfa, 0xeb, 0x02, 0x61, 0xc3, 0x22, 0xb0,
0xfd, 0xfb, 0x9d, 0x32, 0xbb, 0x2f, 0xd8, 0x9d, 0x63, 0x8d, 0x9e, 0xb1,
0x5a, 0x7d, 0xc3, 0xe5, 0xcc, 0xae, 0xe4, 0x71, 0x0d, 0x7b, 0xa9, 0x80,
0xbd, 0xef, 0x86, 0x12, 0x13, 0x42, 0xf8, 0x14, 0xcb, 0xa9, 0xfd, 0x86,
0xea, 0xd5, 0x81, 0x54, 0x87, 0xbc, 0x66, 0x24, 0xfd, 0x12, 0xf5, 0x6b,
0xf4, 0x45, 0x7e, 0x41, 0x8e, 0x00, 0xf3, 0x63, 0xc7, 0x24, 0xc5, 0xa1,
0xfd, 0x70, 0xbc, 0xfa, 0x26, 0xa0, 0x07, 0xc2, 0x97, 0xb1, 0xe0, 0x42,
0x5a, 0x2b, 0x65, 0xec, 0xda, 0x45, 0x79, 0xe5, 0x18, 0xf2, 0xce, 0x8e,
0xc3, 0xe3, 0x74, 0x6d, 0x30, 0x82, 0x01, 0x49, 0x06, 0x09, 0x2a, 0x86,
0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x82, 0x01, 0x3a, 0x04,
0x82, 0x01, 0x36, 0x30, 0x82, 0x01, 0x32, 0x30, 0x82, 0x01, 0x2e, 0x06,
0x0b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x02,
0xa0, 0x81, 0xf7, 0x30, 0x81, 0xf4, 0x30, 0x5f, 0x06, 0x09, 0x2a, 0x86,
0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0d, 0x30, 0x52, 0x30, 0x31, 0x06,
0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0c, 0x30, 0x24,
0x04, 0x10, 0xd8, 0x91, 0xb9, 0x92, 0x3a, 0x1e, 0x6b, 0xa2, 0x57, 0x44,
0x16, 0x3a, 0xea, 0x36, 0x8d, 0x60, 0x02, 0x02, 0x08, 0x00, 0x30, 0x0c,
0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x09, 0x05, 0x00,
0x30, 0x1d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01,
0x2a, 0x04, 0x10, 0x35, 0x48, 0x03, 0x23, 0x6a, 0x3d, 0x5f, 0x96, 0xb7,
0xfb, 0xb1, 0xcb, 0xdc, 0x0c, 0x1f, 0x79, 0x04, 0x81, 0x90, 0x4d, 0x36,
0x5b, 0x1e, 0xc1, 0xc1, 0xa3, 0xfa, 0x85, 0xe6, 0x58, 0x89, 0x5d, 0xbe,
0x17, 0xb4, 0x90, 0x96, 0x2d, 0x3d, 0x32, 0xe2, 0x73, 0x6c, 0x2c, 0xfd,
0x7b, 0xe4, 0x9c, 0x82, 0x3c, 0xe4, 0x7a, 0x58, 0x45, 0xdd, 0xcd, 0x00,
0xa2, 0x52, 0xf9, 0x98, 0xd4, 0x0f, 0x64, 0x0b, 0x58, 0x27, 0xfe, 0xfc,
0xfd, 0x60, 0x3f, 0x7e, 0x6f, 0x73, 0x73, 0xda, 0x16, 0x34, 0x83, 0x38,
0x89, 0x3d, 0xbe, 0xde, 0x0a, 0x47, 0x7b, 0xcd, 0xc1, 0x88, 0x72, 0x38,
0x95, 0xb5, 0x54, 0xfa, 0x2a, 0x84, 0x2b, 0xcd, 0x98, 0xe3, 0x20, 0xd2,
0xac, 0xd8, 0xf5, 0xa8, 0xd2, 0xef, 0x1f, 0x99, 0x88, 0x16, 0x6f, 0xf2,
0x08, 0xe9, 0x28, 0xc8, 0x7a, 0x02, 0x57, 0x5c, 0x1c, 0x94, 0x0f, 0x57,
0xfc, 0x0e, 0x85, 0x63, 0xc5, 0x44, 0x45, 0x4f, 0xc6, 0x35, 0x52, 0x3d,
0xd4, 0xe2, 0x22, 0x6f, 0x72, 0xde, 0x55, 0xad, 0x20, 0x84, 0x91, 0x14,
0x7e, 0x5f, 0x98, 0x2a, 0x5e, 0x2b, 0xf4, 0xc5, 0x10, 0x1b, 0x31, 0x25,
0x30, 0x23, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09,
0x15, 0x31, 0x16, 0x04, 0x14, 0xbd, 0x8a, 0x08, 0x3c, 0x66, 0xf7, 0x9d,
0xa0, 0x25, 0x8b, 0x33, 0xac, 0xc2, 0x5e, 0xdc, 0x76, 0xd4, 0x33, 0x1a,
0xd6, 0x30, 0x41, 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48,
0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20, 0x4e, 0x6f,
0x75, 0x90, 0x57, 0xb4, 0x39, 0x1e, 0xc3, 0x97, 0x50, 0x5e, 0x4d, 0xf2,
0xa8, 0xf9, 0x0e, 0xcd, 0x7f, 0x57, 0x31, 0xae, 0xf2, 0x87, 0x0a, 0x55,
0xb0, 0xbc, 0xd6, 0x11, 0x83, 0xc3, 0x04, 0x08, 0x52, 0x79, 0xb1, 0x83,
0xe6, 0xe4, 0x6e, 0x4d, 0x02, 0x02, 0x08, 0x00,
}};
std::string PollHammerOnce(bool with_recorded_error, bool with_live_objects) {
void* runtime = nullptr;
const int32_t opened = cultmesh_quic_runtime_open("cultmesh-quic-native-tests", &runtime);
if (opened != 0 || runtime == nullptr)
@ -938,6 +1085,36 @@ std::string PollHammerOnce(bool with_recorded_error) {
}
}
// Self's ruling of 2026-09-22, F3: the predicate covered by the hammer set
// above is only ever exercised on a runtime with nothing live in it. An
// unroutable address (RFC 5737 TEST-NET-1) gets a connection into
// `runtime->connections` at once — `cultmesh_quic_connection_open` inserts
// it before the handshake starts — and that handshake then never completes
// inside this scenario's poll window, so it queues no event of its own. A
// best-effort stream rides the same connection. The listener is real (a
// credential MsQuic actually loads) but nothing ever connects to it, so it
// never accepts and never queues an event either: every object here is
// live in the runtime's maps without ever waking the idle poll on its own.
uint64_t live_connection_id = 0;
uint64_t live_stream_id = 0;
uint64_t live_listener_id = 0;
if (with_live_objects) {
const int32_t connecting =
cultmesh_quic_connection_open(runtime, "192.0.2.1", 9, &live_connection_id);
if (connecting != 0) {
cultmesh_quic_runtime_close(runtime);
return "cultmesh_quic_connection_open (live-objects setup) returned " +
std::to_string(connecting);
}
// Best-effort: whether this succeeds is not the point of the
// scenario, and P1's predicate names `connections`, not `streams`.
cultmesh_quic_stream_open(
runtime, live_connection_id, CULTMESH_QUIC_STREAM_RELIABLE, &live_stream_id);
uint16_t bound_port = 0;
cultmesh_quic_listener_open(runtime, "127.0.0.1", 0, kSelfSignedPkcs12.data(),
static_cast<int32_t>(kSelfSignedPkcs12.size()), "", &live_listener_id, &bound_port);
}
std::atomic<bool> hammering{true};
std::thread hammer([runtime, &hammering] {
constexpr uint64_t kBogusId = 0xffffffffffffffffull;
@ -958,6 +1135,9 @@ std::string PollHammerOnce(bool with_recorded_error) {
const TimedPoll poll = PollFor(runtime, kHammerPollMs);
hammering.store(false, std::memory_order_relaxed);
hammer.join();
if (live_listener_id != 0) cultmesh_quic_listener_close(runtime, live_listener_id);
if (live_connection_id != 0) cultmesh_quic_connection_shutdown(runtime, live_connection_id, 0);
(void)live_stream_id;
cultmesh_quic_runtime_close(runtime);
if (poll.result != 0)
@ -974,18 +1154,68 @@ std::string PollHammerOnce(bool with_recorded_error) {
int PollHammer(int iterations) {
for (int iteration = 0; iteration < iterations; ++iteration) {
std::string failure = PollHammerOnce(false);
std::string failure = PollHammerOnce(false, false);
if (!failure.empty())
return Fail("pollhammer iteration " + std::to_string(iteration) + " (clean runtime): " + failure);
failure = PollHammerOnce(true);
failure = PollHammerOnce(true, false);
if (!failure.empty())
return Fail("pollhammer iteration " + std::to_string(iteration) +
" (after a refused call recorded an error): " + failure);
failure = PollHammerOnce(false, true);
if (!failure.empty())
return Fail("pollhammer iteration " + std::to_string(iteration) +
" (with a live connection, stream and listener): " + failure);
}
std::printf("pollhammer %dx: ok\n", iterations);
return 0;
}
// Self's ruling of 2026-09-22, F4: any `timeout_ms <= 0` has to return within a
// small, generously-margined bound, and a negative timeout has to be probed
// too — the bridge's own guard (`timeout_ms > 0`) already treats zero and
// negative alike, so 0, -1 and INT32_MIN all take the same fast path and none
// of them may collide with the seam's own "nothing recorded" sentinel.
constexpr std::array<int32_t, 3> kZeroTimeoutProbesMs{0, -1, INT32_MIN};
// Generous next to `kEarlyToleranceMs`/`kGenerousLateToleranceMs` because a
// non-positive timeout never enters the condition wait at all, so this is
// bounding a runtime lock acquisition and an empty-queue check, not a
// scheduler wakeup — the margin only has to be well clear of a mistake that
// makes this path sleep or spin, not of ordinary jitter.
constexpr int kZeroTimeoutBoundMs = 50;
std::string ZeroTimeoutOnce(int32_t timeout_ms) {
void* runtime = nullptr;
const int32_t opened = cultmesh_quic_runtime_open("cultmesh-quic-native-tests", &runtime);
if (opened != 0 || runtime == nullptr)
return "cultmesh_quic_runtime_open returned " + std::to_string(opened);
const TimedPoll poll = PollFor(runtime, timeout_ms);
cultmesh_quic_runtime_close(runtime);
if (poll.result != 0)
return "a poll asking for " + std::to_string(timeout_ms) + " ms returned " +
std::to_string(poll.result) + ", not the 0 an idle runtime owes it";
if (poll.elapsed_ms > kZeroTimeoutBoundMs)
return "a poll asking for " + std::to_string(timeout_ms) + " ms returned after " +
std::to_string(poll.elapsed_ms) + " ms: a non-positive timeout must return within " +
std::to_string(kZeroTimeoutBoundMs) + " ms, not stall or spin";
return {};
}
int ZeroTimeout(int iterations) {
for (int iteration = 0; iteration < iterations; ++iteration) {
for (int32_t probe : kZeroTimeoutProbesMs) {
const std::string failure = ZeroTimeoutOnce(probe);
if (!failure.empty())
return Fail("zerotimeout iteration " + std::to_string(iteration) + " probe " +
std::to_string(probe) + ": " + failure);
}
}
std::printf("zerotimeout %dx: ok (%zu probes)\n", iterations, kZeroTimeoutProbesMs.size());
return 0;
}
int CloseRace(int iterations, int pollers) {
for (int iteration = 0; iteration < iterations; ++iteration) {
const std::string failure = CloseRaceOnce(pollers);
@ -1001,8 +1231,8 @@ int CloseRace(int iterations, int pollers) {
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr,
"usage: %s closerace|holdclose|holdtimeout|polltimeout|pollbusy|pollhammer|payloadfit|"
"latecall|waitseam [iterations] [pollers]\n",
"usage: %s closerace|holdclose|holdtimeout|polltimeout|pollbusy|pollhammer|zerotimeout|"
"payloadfit|latecall|waitseam [iterations] [pollers]\n",
argv[0]);
return 2;
}
@ -1017,6 +1247,7 @@ int main(int argc, char** argv) {
if (scenario == "polltimeout") return PollTimeout(iterations);
if (scenario == "pollbusy") return PollBusy(iterations);
if (scenario == "pollhammer") return PollHammer(iterations);
if (scenario == "zerotimeout") return ZeroTimeout(iterations);
if (scenario == "payloadfit") return PayloadFit(iterations);
if (scenario == "holdclose" || scenario == "latecall" || scenario == "holdtimeout" ||
scenario == "waitseam") {

View file

@ -468,8 +468,8 @@ const mutations = [
// 50 ms settle, so the poller left before the close began and the scenario
// complained about its own fixture rather than the bridge.
rule: "a positive timeout blocks until an event or the close (loosening: it waits a token 1 ms)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(1)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(1)),",
},
// And the rule underneath that one: the duration waited on is the host's, not
// one of the bridge's own. Self's ruling of 2026-09-22 moved this whole family
@ -485,8 +485,8 @@ const mutations = [
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (revert: a shorter constant of the bridge's own)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(100)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(100)),",
},
{
target: "native",
@ -494,8 +494,8 @@ const mutations = [
// The lengthening, which is the half that looks harmless: every poll still
// blocks, every poll still returns, and nothing anywhere reports an error.
rule: "the wait is on the host's timeout (loosening: a longer constant of the bridge's own)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(2000)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(2000)),",
},
{
target: "native",
@ -504,30 +504,30 @@ const mutations = [
// and the most ordinary spelling this line will ever be given: cap the wait
// so a shutdown gets noticed.
rule: "the wait is on the host's timeout (loosening: the bridge caps it at a second)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::min)(timeout_ms, 1000))),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::min)(timeout_ms, 1000))),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
// The same clamp at a round ceiling nobody would call short.
rule: "the wait is on the host's timeout (loosening: the bridge caps it at a round five seconds)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::min)(timeout_ms, 5000))),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::min)(timeout_ms, 5000))),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: a floor, so a polling host cannot spin)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::max)(timeout_ms, 100))),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::max)(timeout_ms, 100))),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: a little grace added to it)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms + 100)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms + 100)),",
},
// Soul's seventh pass named eight more derivations that were each the
// identity at every wall-clock probe the previous six passes could afford:
@ -541,22 +541,22 @@ const mutations = [
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: a floor at 50, invisible to a late-only check)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms < 50 ? 0 : timeout_ms)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms < 50 ? 0 : timeout_ms)),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: rounded down to a 16 ms quantum)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms / 16 * 16)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms / 16 * 16)),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: rounded up to a 40 ms quantum)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((timeout_ms + 39) / 40 * 40)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((timeout_ms + 39) / 40 * 40)),",
},
{
target: "native",
@ -566,41 +566,41 @@ const mutations = [
// a fault that only shows past the twelfth had nowhere to be seen at all.
rule: "the wait is on the host's timeout (loosening: it doubles once a runtime has polled more than 12 times)",
old: " const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
new: " static std::atomic<int> debug_poll_count{0};\n" +
" const int32_t doubled_timeout_ms = (++debug_poll_count > 12) ? timeout_ms * 2 : timeout_ms;\n" +
" const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(doubled_timeout_ms)),\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(doubled_timeout_ms)),\n" +
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: the bridge caps it at 7275 ms)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::min)(timeout_ms, 7275))),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::min)(timeout_ms, 7275))),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: 25 ms of grace added to it)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms + 25)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms + 25)),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: a floor of 40, so a polling host cannot spin)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::max)(timeout_ms, 40))),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::max)(timeout_ms, 40))),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait is on the host's timeout (loosening: scaled up by 6%)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms * 106 / 100)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms * 106 / 100)),",
},
// The seam itself: development-only, folded away to a pass-through in a
// release build (see the macro's `#else` in cultmesh_quic_native.cpp), and
@ -612,7 +612,7 @@ const mutations = [
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "the wait seam records what the bridge actually waits on (revert: it never records)",
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "std::chrono::milliseconds(timeout_ms),",
},
{
@ -632,10 +632,10 @@ const mutations = [
" // sooner, and `woken` is which of the two happened. RECORD_WAIT wraps\n" +
" // this exact argument; see the development seam note near its macros.\n" +
" const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
new: " std::unique_lock<std::mutex> lock(runtime->gate);\n" +
" CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms);\n" +
" CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms));\n" +
" if (runtime->events.empty() && timeout_ms > 0) {\n" +
" // The host's timeout bounds the wait; an event or the close ends it\n" +
" // sooner, and `woken` is which of the two happened. RECORD_WAIT wraps\n" +
@ -659,10 +659,10 @@ const mutations = [
// milliseconds of its 1000 ms ask.
rule: "an idle poll stays for its timeout while other host threads call (revert: the wait has no predicate)",
old: " const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
new: " const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms))) ==\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms))) ==\n" +
" std::cv_status::no_timeout;\n",
},
{
@ -673,10 +673,10 @@ const mutations = [
// any wake at all. Only a second host thread can see it.
rule: "an idle poll stays for its timeout while other host threads call (loosening: it reads what woke it and does not wait again)",
old: " const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
new: " runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)));\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)));\n" +
" const bool woken = !runtime->events.empty() || runtime->closing;\n",
},
{
@ -689,11 +689,11 @@ const mutations = [
// of the input, so no single probe value makes it the identity.
rule: "an idle poll stays for its timeout while other host threads call (loosening: each wake restarts the whole timeout)",
old: " const bool woken = runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
new: " while (runtime->events.empty() && !runtime->closing &&\n" +
" runtime->signal.wait_for(\n" +
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms))) ==\n" +
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms))) ==\n" +
" std::cv_status::no_timeout) {\n" +
" }\n" +
" const bool woken = !runtime->events.empty() || runtime->closing;\n",
@ -759,11 +759,10 @@ const mutations = [
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
// S5: the same guard at 1500, which `holdtimeout` used to carry at a long
// probe of 1300 and could not see. The long probe is 1600 now, above the
// stated ceiling, with the hold kept armed past it — a call this guard
// parks anyway sits out the rest of the arming instead of leaving on its
// own timeout, well past the scenario's generous late tolerance.
// S5: the same guard at 1500. `holdtimeout`'s long probe sits above it, at
// 1600, with the hold kept armed past it — a call this guard parks anyway
// sits out the rest of the arming instead of leaving on its own timeout,
// well past the scenario's generous late tolerance.
rule: "the hold parks only a call the wait woke (loosening: it also parks any long wait, over 1500)",
old: " if (woken && CULTMESH_QUIC_DEBUG_HELD(lock)) return 0;\n",
new: " if ((woken || timeout_ms > 1500) && CULTMESH_QUIC_DEBUG_HELD(lock)) return 0;\n",
@ -939,6 +938,93 @@ const mutations = [
new: " runtime->signal.wait_for(lock, std::chrono::milliseconds(50),\n" +
" [runtime] { return runtime->active_calls == 0; });",
},
// Self's ruling of 2026-09-22, the eighth fix batch. Six findings, six
// entries below (R1, W1-W3, P1, Z1-Z2); F5 (closerace under starvation) and
// F6 (stale prose) needed no new mutant.
//
// F1: nothing ran the release macro's `#else` branch. `polltimeout`,
// `pollbusy`, `pollhammer` and `zerotimeout` now run against a plain
// release configuration (asserts OFF, CMAKE_BUILD_TYPE=Release) on both
// targets, and R1 changes that branch from a pass-through to a tenth of the
// real timeout — invisible to every configuration that never built it.
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "R1: the release macro is a pass-through (loosening: it divides the duration by 10)",
old: "#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) (duration)",
new: "#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) ((duration)/10)",
},
// F2: the seam now wraps the whole duration handed to `wait_for`, so
// anything that reaches the wait through the seam's own argument is caught
// by `waitseam`'s equality check. These three apply their arithmetic
// outside that argument instead — to the return value of RECORD_WAIT,
// after it has already recorded and returned the honest duration — so
// `waitseam` cannot see them by construction; only the wall-clock
// scenarios can, which is why `kGenerousLateToleranceMs` was narrowed
// alongside this entry.
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "W1: 600 ms added to the wait outside the seam's own argument",
old: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)) + " +
"std::chrono::milliseconds(600),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "W2: the wait floored at 500 ms outside the seam's own argument",
old: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "lock, (std::max)(CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)), " +
"std::chrono::milliseconds(500)),",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "W3: the wait scaled by a 1000/909 ratio outside the seam's own argument",
old: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
new: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)) * 1000 / 909,",
},
// F3: `pollhammer` covered the predicate only on a runtime with nothing
// live in it. P1 adds the survivor: a busy host with a connection is
// treated the same as a busy host with events queued, so it never sleeps.
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "P1: an idle poll also ends when the runtime holds a live connection",
old: " [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
new: " [runtime] {\n" +
" return !runtime->events.empty() || runtime->closing ||\n" +
" !runtime->connections.empty();\n" +
" });\n",
},
// F4: a non-positive timeout was never timed at all — the bridge's own
// guard skips the wait outright, so nothing pinned how fast that path has
// to return. Both entries insert a stall on exactly that path, ahead of
// the guard that already exists.
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "Z1: a non-positive timeout sleeps 500 ms before returning",
old: " if (runtime->events.empty() && timeout_ms > 0) {\n",
new: " if (runtime->events.empty() && timeout_ms <= 0) {\n" +
" std::this_thread::sleep_for(std::chrono::milliseconds(500));\n" +
" }\n" +
" if (runtime->events.empty() && timeout_ms > 0) {\n",
},
{
target: "native",
honestOn: ["linux-x64", "win32-x64"],
rule: "Z2: a non-positive timeout spins for 300 ms before returning",
old: " if (runtime->events.empty() && timeout_ms > 0) {\n",
new: " if (runtime->events.empty() && timeout_ms <= 0) {\n" +
" const auto debug_z2_until =\n" +
" std::chrono::steady_clock::now() + std::chrono::milliseconds(300);\n" +
" while (std::chrono::steady_clock::now() < debug_z2_until) {\n" +
" }\n" +
" }\n" +
" if (runtime->events.empty() && timeout_ms > 0) {\n",
},
];
function digest(bytes) {
@ -965,23 +1051,28 @@ function testsPass(workspace) {
// the scenario runner beside it and run the scenarios. There is no npm here and
// no `dist/`; the artifacts live under artifacts/, which is git-ignored.
//
// Two configurations, because the two rules are visible to different things. The
// assertion build states the quiesce invariant outright, which is what makes the
// missing wait fail on the first close instead of on an unlucky one. The
// ThreadSanitizer build is the only thing that sees a notify land on a destroyed
// condition variable, and it exists on linux-x64 alone.
// Two or three configurations, because the rules are visible to different
// things. The assertion build states the quiesce invariant outright, which is
// what makes the missing wait fail on the first close instead of on an unlucky
// one. The ThreadSanitizer build is the only thing that sees a notify land on
// a destroyed condition variable, and it exists on linux-x64 alone. The
// release build is the thing that actually ships: asserts off, no development
// seam, no sanitizer — added by Self's ruling of 2026-09-22 because it is the
// one configuration where R1's release-only macro branch is reachable at all.
//
// `holdclose`, `latecall` and `holdtimeout` run only where the development seam
// exists, which is the assertion build: they drive the quiesce and the refusal
// through that seam rather than through a sleep, and they are what kill the
// rules about counting a host call at all. The ThreadSanitizer build
// deliberately keeps the shipped shape — no assertions, no seam — because what
// it is there for is the race in a library built like the one that ships.
// it is there for is the race in a library built like the one that ships, and
// the release build keeps that same shape for the rules that ship-shape alone
// can hide.
//
// `polltimeout`, `pollbusy` and `holdtimeout` measure a wait against the
// timeout it asked for, so they are run only where nothing distorts the clock:
// a sanitizer's slowdown would make their bands meaningless, which is the honest
// limit of those entries.
// `polltimeout`, `pollbusy`, `pollhammer` and `holdtimeout` measure a wait
// against the timeout it asked for, so they are run only where nothing
// distorts the clock: a sanitizer's slowdown would make their bands
// meaningless, which is the honest limit of those entries.
const assertScenarios = [
["holdclose", "3", "64"],
["latecall", "5"],
@ -990,15 +1081,34 @@ const assertScenarios = [
["polltimeout", "3"],
["pollbusy", "3"],
["pollhammer", "3"],
["zerotimeout", "3"],
["payloadfit", "3"],
["closerace", "20", "256"],
];
// Self's ruling of 2026-09-22, F1: the assertion build is the only
// configuration that ever ran the timed scenarios, so R1's release-only
// `#else` branch (cultmesh_quic_native.cpp's CULTMESH_QUIC_DEBUG_RECORD_WAIT
// macro) was unobservable by construction — nothing built without
// CULTMESH_QUIC_DEBUG_ASSERTS ever ran `polltimeout`, `pollbusy` or
// `pollhammer` against it. This configuration is the thing that ships: a
// plain Release build, asserts off, no sanitizer, running the timed
// scenarios plus the new zero-timeout check against it.
const releaseScenarios = [
["polltimeout", "3"],
["pollbusy", "3"],
["pollhammer", "3"],
["zerotimeout", "3"],
];
const nativeConfigurations = platform === "linux-x64"
? [
{ name: "asserts", asserts: "ON", sanitizer: null, scenarios: assertScenarios },
{ name: "tsan", asserts: "OFF", sanitizer: "thread", scenarios: [["closerace", "20", "256"]] },
{ name: "release", asserts: "OFF", sanitizer: null, buildType: "Release", scenarios: releaseScenarios },
]
: [{ name: "asserts", asserts: "ON", sanitizer: null, scenarios: assertScenarios }];
: [
{ name: "asserts", asserts: "ON", sanitizer: null, scenarios: assertScenarios },
{ name: "release", asserts: "OFF", sanitizer: null, buildType: "Release", scenarios: releaseScenarios },
];
function msquicArguments() {
if (platform === "win32-x64") {
@ -1087,10 +1197,14 @@ function nativeScenariosPass() {
const flags = configuration.sanitizer
? [`-fsanitize=${configuration.sanitizer}`, "-fno-omit-frame-pointer", "-g"].join(" ")
: "";
// The release configuration builds Release rather than RelWithDebInfo — it
// is the thing that actually ships, and R1 targets the macro branch that
// only a build without CULTMESH_QUIC_DEBUG_ASSERTS ever compiles.
const buildType = configuration.buildType ?? "RelWithDebInfo";
// Multi-config generators (MSVC) put the binaries in a per-config directory;
// single-config ones (Ninja, Makefiles) do not.
const binaries = platform === "win32-x64"
? join(build, "bin", "RelWithDebInfo")
? join(build, "bin", buildType)
: join(build, "bin");
try {
execFileSync("cmake", [
@ -1099,7 +1213,7 @@ function nativeScenariosPass() {
// 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_BUILD_TYPE=${buildType}`,
`-DCMAKE_CXX_FLAGS=${flags}`,
`-DCMAKE_EXE_LINKER_FLAGS=${flags}`,
`-DCMAKE_SHARED_LINKER_FLAGS=${flags}`,
@ -1107,7 +1221,7 @@ function nativeScenariosPass() {
`-DCULTMESH_QUIC_DEBUG_ASSERTS=${configuration.asserts}`,
...msquic.configure,
], { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] });
execFileSync("cmake", ["--build", build, "--config", "RelWithDebInfo"],
execFileSync("cmake", ["--build", build, "--config", buildType],
{ cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] });
} catch (error) {
// A mutation that does not compile is killed by the build, which is a